feat: implement WeChat MP OAuth login
- Add wechat-mp.service.ts for MP web authorization - Add wechat-mp.routes.ts with /api/mp endpoints - Update EntryQRCode.vue to show H5 URL QR code - Update HomeView.vue with WeChat auth detection Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,16 +1,120 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { ref, onMounted } from 'vue';
|
||||
import { useRouter, useRoute } from 'vue-router';
|
||||
import { showLoadingToast, closeToast, showToast } from 'vant';
|
||||
import { useConnectionStore } from '../stores/connection';
|
||||
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
const connectionStore = useConnectionStore();
|
||||
|
||||
// Check if already logged in
|
||||
onMounted(() => {
|
||||
const isProcessing = ref(false);
|
||||
|
||||
// API base URL
|
||||
const apiUrl = import.meta.env.VITE_API_URL || 'http://192.168.1.5:3000';
|
||||
|
||||
/**
|
||||
* 检测是否在微信环境中
|
||||
*/
|
||||
function isWechatBrowser(): boolean {
|
||||
const ua = navigator.userAgent.toLowerCase();
|
||||
return ua.includes('micromessenger');
|
||||
}
|
||||
|
||||
/**
|
||||
* 用code完成登录
|
||||
*/
|
||||
async function loginWithCode(code: string) {
|
||||
isProcessing.value = true;
|
||||
showLoadingToast({ message: '登录中...', forbidClick: true });
|
||||
|
||||
try {
|
||||
const response = await fetch(`${apiUrl}/api/mp/login`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ code }),
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
closeToast();
|
||||
|
||||
if (result.success && result.data) {
|
||||
// 设置用户信息
|
||||
connectionStore.setUser(
|
||||
result.data.userId,
|
||||
result.data.userName,
|
||||
result.data.sessionToken
|
||||
);
|
||||
|
||||
showToast({ message: '登录成功!', type: 'success' });
|
||||
|
||||
// 跳转到投票页面
|
||||
router.replace('/vote');
|
||||
} else {
|
||||
showToast({ message: result.error || '登录失败', type: 'fail' });
|
||||
isProcessing.value = false;
|
||||
}
|
||||
} catch (err) {
|
||||
closeToast();
|
||||
showToast({ message: '网络错误,请重试', type: 'fail' });
|
||||
isProcessing.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 跳转到微信授权页面
|
||||
*/
|
||||
async function redirectToWechatAuth() {
|
||||
try {
|
||||
// 获取授权URL
|
||||
const currentUrl = window.location.href.split('?')[0]; // 移除已有的query参数
|
||||
const response = await fetch(
|
||||
`${apiUrl}/api/mp/auth-url?redirect_uri=${encodeURIComponent(currentUrl)}&scope=snsapi_base`
|
||||
);
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success && result.data?.authUrl) {
|
||||
// 跳转到微信授权页面
|
||||
window.location.href = result.data.authUrl;
|
||||
} else {
|
||||
console.error('[HomeView] Failed to get auth URL:', result.error);
|
||||
showToast({ message: '获取授权链接失败', type: 'fail' });
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('[HomeView] Failed to redirect to auth:', err);
|
||||
showToast({ message: '网络错误', type: 'fail' });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理微信授权流程
|
||||
*/
|
||||
async function handleWechatAuth() {
|
||||
// 检查URL中是否有code参数(授权回调)
|
||||
const code = route.query.code as string;
|
||||
|
||||
if (code) {
|
||||
// 有code,用code完成登录
|
||||
console.log('[HomeView] Got code from callback, logging in...');
|
||||
await loginWithCode(code);
|
||||
} else {
|
||||
// 无code,跳转到授权页面
|
||||
console.log('[HomeView] No code, redirecting to auth...');
|
||||
await redirectToWechatAuth();
|
||||
}
|
||||
}
|
||||
|
||||
// 页面加载时检查登录状态和微信环境
|
||||
onMounted(async () => {
|
||||
// 已登录则直接跳转
|
||||
if (connectionStore.userId && connectionStore.userName && connectionStore.userName !== '访客') {
|
||||
// Already logged in, redirect to vote page
|
||||
router.replace('/vote');
|
||||
return;
|
||||
}
|
||||
|
||||
// 在微信环境中自动处理授权
|
||||
if (isWechatBrowser()) {
|
||||
await handleWechatAuth();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onUnmounted, computed } from 'vue';
|
||||
import { ref, onMounted, onUnmounted } from 'vue';
|
||||
import QRCode from 'qrcode';
|
||||
import { useDisplayStore } from '../stores/display';
|
||||
import type { WechatLoginSuccessPayload } from '@gala/shared/types';
|
||||
|
||||
const props = defineProps<{
|
||||
mobileUrl: string;
|
||||
@@ -10,59 +8,21 @@ const props = defineProps<{
|
||||
|
||||
const emit = defineEmits<{
|
||||
close: [];
|
||||
loginSuccess: [payload: WechatLoginSuccessPayload];
|
||||
}>();
|
||||
|
||||
const displayStore = useDisplayStore();
|
||||
|
||||
const qrCodeDataUrl = ref<string>('');
|
||||
const isLoading = ref(true);
|
||||
const error = ref<string | null>(null);
|
||||
const wechatAuthUrl = ref<string | null>(null);
|
||||
const loginState = ref<string | null>(null);
|
||||
const qrCodeDataUrl = ref<string>('');
|
||||
|
||||
// Use WeChat auth URL if available, otherwise fallback to mobile URL
|
||||
const qrUrl = computed(() => wechatAuthUrl.value || props.mobileUrl);
|
||||
|
||||
// Fetch WeChat login URL from server
|
||||
async function fetchWechatLoginUrl() {
|
||||
// 生成移动端H5入口二维码
|
||||
async function generateQRCode() {
|
||||
isLoading.value = true;
|
||||
error.value = null;
|
||||
|
||||
try {
|
||||
const socket = displayStore.getSocket();
|
||||
const socketId = socket?.id;
|
||||
|
||||
if (!socketId) {
|
||||
console.warn('[EntryQRCode] Socket not connected, using fallback URL');
|
||||
await generateQRCode(props.mobileUrl);
|
||||
return;
|
||||
}
|
||||
|
||||
const apiUrl = import.meta.env.VITE_API_URL || '';
|
||||
const response = await fetch(`${apiUrl}/api/wechat/login?pcSocketId=${socketId}`);
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success && result.data) {
|
||||
wechatAuthUrl.value = result.data.authUrl;
|
||||
loginState.value = result.data.state;
|
||||
await generateQRCode(result.data.authUrl);
|
||||
console.log('[EntryQRCode] WeChat auth URL obtained');
|
||||
} else {
|
||||
console.warn('[EntryQRCode] WeChat not configured, using fallback URL');
|
||||
await generateQRCode(props.mobileUrl);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('[EntryQRCode] Failed to fetch WeChat login URL:', err);
|
||||
// Fallback to mobile URL
|
||||
await generateQRCode(props.mobileUrl);
|
||||
}
|
||||
}
|
||||
|
||||
// Generate QR code
|
||||
async function generateQRCode(url: string) {
|
||||
try {
|
||||
qrCodeDataUrl.value = await QRCode.toDataURL(url, {
|
||||
// 直接使用移动端URL生成二维码
|
||||
// 用户扫码后在微信内打开H5,H5会自动检测微信环境并跳转授权
|
||||
qrCodeDataUrl.value = await QRCode.toDataURL(props.mobileUrl, {
|
||||
width: 400,
|
||||
margin: 2,
|
||||
color: {
|
||||
@@ -70,21 +30,15 @@ async function generateQRCode(url: string) {
|
||||
light: '#ffffff',
|
||||
},
|
||||
});
|
||||
isLoading.value = false;
|
||||
console.log('[EntryQRCode] QR code generated for:', props.mobileUrl);
|
||||
} catch (err) {
|
||||
console.error('Failed to generate QR code:', err);
|
||||
console.error('[EntryQRCode] Failed to generate QR code:', err);
|
||||
error.value = '生成二维码失败';
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Handle WeChat login success event
|
||||
function handleWechatLoginSuccess(payload: WechatLoginSuccessPayload) {
|
||||
console.log('[EntryQRCode] WeChat login success:', payload);
|
||||
emit('loginSuccess', payload);
|
||||
emit('close');
|
||||
}
|
||||
|
||||
// Handle ESC key to close
|
||||
function handleKeydown(e: KeyboardEvent) {
|
||||
if (e.key === 'Escape') {
|
||||
@@ -92,30 +46,12 @@ function handleKeydown(e: KeyboardEvent) {
|
||||
}
|
||||
}
|
||||
|
||||
// Setup WebSocket listener for login success
|
||||
function setupSocketListener() {
|
||||
const socket = displayStore.getSocket();
|
||||
if (socket) {
|
||||
socket.on('wechat:login_success' as any, handleWechatLoginSuccess);
|
||||
}
|
||||
}
|
||||
|
||||
// Cleanup WebSocket listener
|
||||
function cleanupSocketListener() {
|
||||
const socket = displayStore.getSocket();
|
||||
if (socket) {
|
||||
socket.off('wechat:login_success' as any, handleWechatLoginSuccess);
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchWechatLoginUrl();
|
||||
setupSocketListener();
|
||||
generateQRCode();
|
||||
window.addEventListener('keydown', handleKeydown);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
cleanupSocketListener();
|
||||
window.removeEventListener('keydown', handleKeydown);
|
||||
});
|
||||
</script>
|
||||
@@ -140,7 +76,7 @@ onUnmounted(() => {
|
||||
</div>
|
||||
|
||||
<!-- WeChat logo hint -->
|
||||
<div v-if="wechatAuthUrl" class="wechat-hint">
|
||||
<div class="wechat-hint">
|
||||
<svg class="wechat-icon" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M8.691 2.188C3.891 2.188 0 5.476 0 9.53c0 2.212 1.17 4.203 3.002 5.55a.59.59 0 0 1 .213.665l-.39 1.48c-.019.07-.048.141-.048.213 0 .163.13.295.29.295a.326.326 0 0 0 .167-.054l1.903-1.114a.864.864 0 0 1 .717-.098 10.16 10.16 0 0 0 2.837.403c.276 0 .543-.027.811-.05-.857-2.578.157-4.972 1.932-6.446 1.703-1.415 3.882-1.98 5.853-1.838-.576-3.583-4.196-6.348-8.596-6.348zM5.785 5.991c.642 0 1.162.529 1.162 1.18a1.17 1.17 0 0 1-1.162 1.178A1.17 1.17 0 0 1 4.623 7.17c0-.651.52-1.18 1.162-1.18zm5.813 0c.642 0 1.162.529 1.162 1.18a1.17 1.17 0 0 1-1.162 1.178 1.17 1.17 0 0 1-1.162-1.178c0-.651.52-1.18 1.162-1.18zm5.34 2.867c-1.797-.052-3.746.512-5.28 1.786-1.72 1.428-2.687 3.72-1.78 6.22.942 2.453 3.666 4.229 6.884 4.229.826 0 1.622-.12 2.361-.336a.722.722 0 0 1 .598.082l1.584.926a.272.272 0 0 0 .14.047c.134 0 .24-.111.24-.247 0-.06-.023-.12-.038-.177l-.327-1.233a.582.582 0 0 1-.023-.156.49.49 0 0 1 .201-.398C23.024 18.48 24 16.82 24 14.98c0-3.21-2.931-5.837-6.656-6.088V8.89c-.135-.01-.269-.03-.406-.03zm-2.53 3.274c.535 0 .969.44.969.982a.976.976 0 0 1-.969.983.976.976 0 0 1-.969-.983c0-.542.434-.982.97-.982zm4.844 0c.535 0 .969.44.969.982a.976.976 0 0 1-.969.983.976.976 0 0 1-.969-.983c0-.542.434-.982.969-.982z"/>
|
||||
</svg>
|
||||
@@ -155,11 +91,11 @@ onUnmounted(() => {
|
||||
</div>
|
||||
<div class="step">
|
||||
<span class="step-num">2</span>
|
||||
<span class="step-text">{{ wechatAuthUrl ? '授权登录' : '填写姓名和部门' }}</span>
|
||||
<span class="step-text">确认授权登录</span>
|
||||
</div>
|
||||
<div class="step">
|
||||
<span class="step-num">3</span>
|
||||
<span class="step-text">{{ wechatAuthUrl ? '自动进入年会' : '点击进入年会' }}</span>
|
||||
<span class="step-text">自动进入年会</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -3,6 +3,8 @@ import cors from 'cors';
|
||||
import helmet from 'helmet';
|
||||
import compression from 'compression';
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { dirname } from 'path';
|
||||
import { config } from './config';
|
||||
import { logger } from './utils/logger';
|
||||
import { errorHandler } from './middleware/errorHandler';
|
||||
@@ -11,6 +13,12 @@ import voteRoutes from './routes/vote.routes';
|
||||
import adminRoutes from './routes/admin.routes';
|
||||
import scanRoutes from './routes/scan.routes';
|
||||
import wechatRoutes from './routes/wechat.routes';
|
||||
import wechatMpRoutes from './routes/wechat-mp.routes';
|
||||
import publicRoutes from './routes/public.routes';
|
||||
|
||||
// ES Module __dirname equivalent
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
|
||||
export const app: Application = express();
|
||||
|
||||
@@ -65,6 +73,8 @@ app.use('/api/vote', voteRoutes);
|
||||
app.use('/api/admin', adminRoutes);
|
||||
app.use('/api/scan', scanRoutes);
|
||||
app.use('/api/wechat', wechatRoutes);
|
||||
app.use('/api/mp', wechatMpRoutes);
|
||||
app.use('/api/public', publicRoutes);
|
||||
|
||||
// 404 handler
|
||||
app.use((_req, res) => {
|
||||
|
||||
109
packages/server/src/routes/wechat-mp.routes.ts
Normal file
109
packages/server/src/routes/wechat-mp.routes.ts
Normal file
@@ -0,0 +1,109 @@
|
||||
import { Router, Request, Response } from 'express';
|
||||
import { wechatMpService } from '../services/wechat-mp.service';
|
||||
import { config } from '../config';
|
||||
import { logger } from '../utils/logger';
|
||||
|
||||
const router = Router();
|
||||
|
||||
/**
|
||||
* GET /api/mp/auth-url
|
||||
* 获取公众号网页授权URL
|
||||
* 前端在微信环境中调用此接口,获取授权跳转URL
|
||||
*/
|
||||
router.get('/auth-url', (req: Request, res: Response) => {
|
||||
try {
|
||||
if (!wechatMpService.isConfigured()) {
|
||||
return res.json({
|
||||
success: false,
|
||||
error: 'WeChat MP not configured',
|
||||
});
|
||||
}
|
||||
|
||||
// 从query获取回调地址,默认使用移动端URL
|
||||
const redirectUri = (req.query.redirect_uri as string) || config.mobileClientUrl;
|
||||
|
||||
// 生成随机state防止CSRF
|
||||
const state = Math.random().toString(36).slice(2, 15);
|
||||
|
||||
// 使用snsapi_base静默授权(只获取openid)
|
||||
const scope = (req.query.scope as 'snsapi_base' | 'snsapi_userinfo') || 'snsapi_base';
|
||||
|
||||
const authUrl = wechatMpService.generateAuthUrl(redirectUri, state, scope);
|
||||
|
||||
logger.info({ redirectUri, scope }, 'Generated WeChat MP auth URL');
|
||||
|
||||
return res.json({
|
||||
success: true,
|
||||
data: {
|
||||
authUrl,
|
||||
state,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error({ error }, 'Failed to generate auth URL');
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
error: 'Internal server error',
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* POST /api/mp/login
|
||||
* 用code完成登录
|
||||
* 前端在授权回调后,携带code调用此接口完成登录
|
||||
*/
|
||||
router.post('/login', async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { code } = req.body;
|
||||
|
||||
if (!code) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: 'Missing code parameter',
|
||||
});
|
||||
}
|
||||
|
||||
if (!wechatMpService.isConfigured()) {
|
||||
return res.json({
|
||||
success: false,
|
||||
error: 'WeChat MP not configured',
|
||||
});
|
||||
}
|
||||
|
||||
const result = await wechatMpService.login(code);
|
||||
|
||||
if (!result.success) {
|
||||
return res.json({
|
||||
success: false,
|
||||
error: result.error,
|
||||
});
|
||||
}
|
||||
|
||||
return res.json({
|
||||
success: true,
|
||||
data: result.data,
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error({ error }, 'Failed to process MP login');
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
error: 'Internal server error',
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* GET /api/mp/config
|
||||
* 获取公众号配置状态(不返回敏感信息)
|
||||
*/
|
||||
router.get('/config', (_req: Request, res: Response) => {
|
||||
return res.json({
|
||||
success: true,
|
||||
data: {
|
||||
configured: wechatMpService.isConfigured(),
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
export default router;
|
||||
173
packages/server/src/services/wechat-mp.service.ts
Normal file
173
packages/server/src/services/wechat-mp.service.ts
Normal file
@@ -0,0 +1,173 @@
|
||||
import { config } from '../config';
|
||||
import { logger } from '../utils/logger';
|
||||
import { createSessionToken } from '../utils/auth';
|
||||
import type { WechatAccessTokenResponse, WechatUserInfo } from '@gala/shared/types';
|
||||
|
||||
const SESSION_TOKEN_TTL = 24 * 60 * 60; // 24 hours
|
||||
|
||||
/**
|
||||
* 微信公众号网页授权服务
|
||||
* 用于在微信内H5页面进行OAuth授权登录
|
||||
*/
|
||||
class WechatMpService {
|
||||
private readonly appId: string;
|
||||
private readonly appSecret: string;
|
||||
|
||||
constructor() {
|
||||
this.appId = config.wechatMp.appId;
|
||||
this.appSecret = config.wechatMp.appSecret;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查公众号配置是否有效
|
||||
*/
|
||||
isConfigured(): boolean {
|
||||
return !!(this.appId && this.appSecret);
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成公众号网页授权URL
|
||||
* @param redirectUri 授权后回调地址
|
||||
* @param state 状态参数,用于防止CSRF
|
||||
* @param scope 授权作用域: snsapi_base(静默授权) 或 snsapi_userinfo(需用户确认)
|
||||
*/
|
||||
generateAuthUrl(
|
||||
redirectUri: string,
|
||||
state: string,
|
||||
scope: 'snsapi_base' | 'snsapi_userinfo' = 'snsapi_base'
|
||||
): string {
|
||||
const baseUrl = 'https://open.weixin.qq.com/connect/oauth2/authorize';
|
||||
const params = new URLSearchParams({
|
||||
appid: this.appId,
|
||||
redirect_uri: redirectUri,
|
||||
response_type: 'code',
|
||||
scope: scope,
|
||||
state: state,
|
||||
});
|
||||
return `${baseUrl}?${params.toString()}#wechat_redirect`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 用code换取access_token和openid
|
||||
*/
|
||||
async getAccessToken(code: string): Promise<{
|
||||
success: boolean;
|
||||
data?: { openid: string; accessToken: string };
|
||||
error?: string;
|
||||
}> {
|
||||
try {
|
||||
const url = 'https://api.weixin.qq.com/sns/oauth2/access_token';
|
||||
const params = new URLSearchParams({
|
||||
appid: this.appId,
|
||||
secret: this.appSecret,
|
||||
code: code,
|
||||
grant_type: 'authorization_code',
|
||||
});
|
||||
|
||||
const response = await fetch(`${url}?${params.toString()}`);
|
||||
const data = (await response.json()) as WechatAccessTokenResponse;
|
||||
|
||||
if (data.errcode) {
|
||||
logger.error({ errcode: data.errcode, errmsg: data.errmsg }, 'WeChat MP access_token error');
|
||||
return { success: false, error: data.errmsg || 'Failed to get access token' };
|
||||
}
|
||||
|
||||
if (!data.openid || !data.access_token) {
|
||||
return { success: false, error: 'Invalid response from WeChat' };
|
||||
}
|
||||
|
||||
logger.info({ openid: data.openid }, 'WeChat MP access_token obtained');
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: { openid: data.openid, accessToken: data.access_token },
|
||||
};
|
||||
} catch (error) {
|
||||
logger.error({ error }, 'Failed to get WeChat MP access token');
|
||||
return { success: false, error: 'Failed to get access token' };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户信息(snsapi_userinfo时可用)
|
||||
*/
|
||||
async getUserInfo(accessToken: string, openid: string): Promise<WechatUserInfo | null> {
|
||||
try {
|
||||
const url = 'https://api.weixin.qq.com/sns/userinfo';
|
||||
const params = new URLSearchParams({
|
||||
access_token: accessToken,
|
||||
openid: openid,
|
||||
lang: 'zh_CN',
|
||||
});
|
||||
|
||||
const response = await fetch(`${url}?${params.toString()}`);
|
||||
const data = (await response.json()) as { errcode?: number; errmsg?: string } & WechatUserInfo;
|
||||
|
||||
if (data.errcode) {
|
||||
logger.error({ errcode: data.errcode, errmsg: data.errmsg }, 'WeChat MP userinfo error');
|
||||
return null;
|
||||
}
|
||||
|
||||
return data as WechatUserInfo;
|
||||
} catch (error) {
|
||||
logger.error({ error }, 'Failed to get WeChat MP user info');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 用code完成登录,返回sessionToken
|
||||
*/
|
||||
async login(code: string): Promise<{
|
||||
success: boolean;
|
||||
data?: {
|
||||
openid: string;
|
||||
sessionToken: string;
|
||||
userId: string;
|
||||
userName: string;
|
||||
userInfo?: WechatUserInfo;
|
||||
};
|
||||
error?: string;
|
||||
}> {
|
||||
try {
|
||||
// 用code换取access_token
|
||||
const tokenResult = await this.getAccessToken(code);
|
||||
if (!tokenResult.success || !tokenResult.data) {
|
||||
return { success: false, error: tokenResult.error };
|
||||
}
|
||||
|
||||
const { openid, accessToken } = tokenResult.data;
|
||||
|
||||
// 尝试获取用户信息(如果是snsapi_userinfo授权)
|
||||
const userInfo = await this.getUserInfo(accessToken, openid);
|
||||
|
||||
// 生成用户ID和名称
|
||||
const userId = `wx_${openid.slice(0, 16)}`;
|
||||
const userName = userInfo?.nickname || `微信用户_${openid.slice(-6)}`;
|
||||
|
||||
// 创建session token
|
||||
const sessionToken = await createSessionToken(
|
||||
{ userId, userName, openid, role: 'user' },
|
||||
SESSION_TOKEN_TTL
|
||||
);
|
||||
|
||||
logger.info({ openid, userId }, 'WeChat MP login successful');
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
openid,
|
||||
sessionToken,
|
||||
userId,
|
||||
userName,
|
||||
userInfo: userInfo || undefined,
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
logger.error({ error }, 'Failed to process WeChat MP login');
|
||||
return { success: false, error: 'Failed to process login' };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const wechatMpService = new WechatMpService();
|
||||
Reference in New Issue
Block a user