diff --git a/src/api/passport/wx-login/index.ts b/src/api/passport/wx-login/index.ts new file mode 100644 index 0000000..4213e09 --- /dev/null +++ b/src/api/passport/wx-login/index.ts @@ -0,0 +1,123 @@ +import request from '@/utils/request'; +import type { ApiResult } from '@/api'; +import { SERVER_API_URL } from '@/utils/server'; + +/** + * 微信小程序登录相关接口 + */ + +// 微信登录请求参数 +export interface WxLoginParam { + // 微信登录凭证(wx.login() 获取) + code: string; + // 租户ID + tenantId?: number; +} + +// 微信登录用户信息 +export interface WxLoginUserInfo { + userId: number; + username?: string; + nickname?: string; + avatar?: string; + phone?: string; + openid?: string; + unionid?: string; + tenantId: number; +} + +// 微信登录结果 +export interface WxLoginResult { + // JWT 访问令牌 + access_token?: string; + // 用户信息 + user?: WxLoginUserInfo; + // 租户ID + tenantId?: number; + // 登录消息 + message?: string; + // 用户ID + userId?: number; +} + +/** + * 微信小程序登录(通过 openid) + * + * 流程: + * 1. 调用 wx.login() 获取 code + * 2. 用 code 调用此接口 + * 3. 后端用 code 换取 openid,查库返回用户信息 + * 4. 如果用户存在,返回登录 token + * 5. 如果用户不存在,返回 "用户未注册" + openid + */ +export async function loginByOpenId(data: WxLoginParam): Promise<{ + success: boolean; + message: string; + data?: WxLoginResult; + openid?: string; +}> { + const res = await request.post>( + SERVER_API_URL + '/wx-login/loginByOpenId', + data + ); + + console.log('[WxLogin] loginByOpenId 响应:', res); + + // 成功:用户已注册 + if ((res.code === 0 || res.code === 200) && res.data) { + return { + success: true, + message: res.message || '登录成功', + data: res.data + }; + } + + // 失败:用户未注册或登录失败 + // 后端返回 "用户未注册" 时,data 字段是 openid + if (res.message === '用户未注册' || res.message?.includes('用户未注册')) { + return { + success: false, + message: '用户未注册', + openid: res.data as unknown as string + }; + } + + return { + success: false, + message: res.message || '登录失败' + }; +} + +/** + * 获取微信 OpenId(仅获取,不登录) + */ +export async function getOpenId(code: string): Promise<{ + success: boolean; + openid?: string; + unionid?: string; + session_key?: string; + message?: string; +}> { + const res = await request.post>( + SERVER_API_URL + '/wx-login/getOpenId', + { code } + ); + + if ((res.code === 0 || res.code === 200) && res.data) { + return { + success: true, + openid: res.data.openid, + unionid: res.data.unionid, + session_key: res.data.session_key + }; + } + + return { + success: false, + message: res.message || '获取 OpenId 失败' + }; +} diff --git a/src/passport/qr-confirm/index.tsx b/src/passport/qr-confirm/index.tsx index ec58658..d9741b4 100644 --- a/src/passport/qr-confirm/index.tsx +++ b/src/passport/qr-confirm/index.tsx @@ -4,7 +4,7 @@ import { Button, Loading, Card } from '@nutui/nutui-react-taro'; import { Success, Failure, Tips, User } from '@nutui/icons-react-taro'; import Taro, { useRouter } from '@tarojs/taro'; import { confirmQRLogin } from '@/api/passport/qr-login'; -import { useUser } from '@/hooks/useUser'; +import { loginByOpenId } from '@/api/passport/wx-login'; /** * 扫码登录确认页面 @@ -17,15 +17,23 @@ import { useUser } from '@/hooks/useUser'; * - 微信「扫普通链接二维码打开小程序」配置的二维码规则:`https://websopy.websoft.top/wx-scan/` * - 扫码后 URL:`https://websopy.websoft.top/wx-scan?token=xxx` * - 小程序接收到参数后自动确认登录 + * + * 登录流程(2026-04-07 实现): + * 1. 用户扫码 → 进入 qr-confirm 页面 + * 2. 页面立即调用 wx.login() 获取 code + * 3. 用 code 调用 /api/wx-login/loginByOpenId 获取/验证用户身份 + * 4. 如果用户不存在 → 跳转到用户页引导注册 + * 5. 如果用户存在 → 自动调用 confirmQRLogin 确认登录 */ const QRConfirmPage: React.FC = () => { const router = useRouter(); - const { user, getDisplayName, isLoggedIn } = useUser(); + // 移除 useUser 依赖,改用 wx.login() + loginByOpenId 方式验证用户身份 const [loading, setLoading] = useState(false); const [confirmed, setConfirmed] = useState(false); const [error, setError] = useState(''); const [token, setToken] = useState(''); const [loginMethod, setLoginMethod] = useState<'scan' | 'url'>('url'); + const [userInfo, setUserInfo] = useState(null); useEffect(() => { // 从 URL 参数中获取 token @@ -80,32 +88,74 @@ const QRConfirmPage: React.FC = () => { /** * 自动确认登录(URL 扫码场景) + * + * 新的登录流程(2026-04-07): + * 1. 调用 wx.login() 获取 code + * 2. 用 code 调用后端 loginByOpenId 接口验证用户身份 + * 3. 用户存在 → 调用 confirmQRLogin 确认登录 + * 4. 用户不存在 → 跳转到用户页引导注册 */ const handleAutoConfirm = async (loginToken: string) => { - if (!isLoggedIn || !user?.userId) { - // 用户未登录,跳转到登录页面 - console.log('[QRConfirm] 用户未登录,跳转到登录页'); - Taro.showToast({ - title: '请先登录小程序', - icon: 'none', - duration: 2000 + try { + setLoading(true); + + // 1. 调用微信登录获取 code + console.log('[QRConfirm] 调用 wx.login() 获取 code...'); + const loginResult = await Taro.login(); + + if (!loginResult.code) { + throw new Error('获取微信登录凭证失败'); + } + console.log('[QRConfirm] 获取到 code:', loginResult.code); + + // 2. 用 code 调用后端接口验证用户身份 + console.log('[QRConfirm] 调用后端 loginByOpenId...'); + const wxLoginResult = await loginByOpenId({ + code: loginResult.code, + tenantId: 10398 // 使用固定租户ID }); - setTimeout(() => { - Taro.redirectTo({ - url: '/passport/login' + console.log('[QRConfirm] loginByOpenId 结果:', wxLoginResult); + + // 3. 判断用户是否存在 + if (wxLoginResult.success && wxLoginResult.data) { + // 用户已注册,保存用户信息并继续确认登录 + console.log('[QRConfirm] 用户已注册,开始确认登录...'); + setUserInfo(wxLoginResult.data.user); + + // 调用确认登录 + await handleConfirmLogin(loginToken, wxLoginResult.data.user); + } else { + // 用户未注册,跳转到用户页引导注册 + console.log('[QRConfirm] 用户未注册,跳转到用户页引导注册'); + + Taro.showToast({ + title: '请先注册/登录小程序', + icon: 'none', + duration: 2000 }); - }, 2000); - return; + + setTimeout(() => { + // 跳转到用户中心,在那里可以完成注册和登录 + Taro.switchTab({ + url: '/pages/user/user' + }); + }, 2000); + } + } catch (err: any) { + console.error('[QRConfirm] 自动确认登录失败:', err); + setError(err.message || '自动确认登录失败'); + } finally { + setLoading(false); } - - await handleConfirmLogin(loginToken); }; /** * 确认登录 + * @param loginToken - 可选的登录token,默认使用页面token + * @param wxUserInfo - 微信登录获取的用户信息(可选) */ - const handleConfirmLogin = async (loginToken?: string) => { + const handleConfirmLogin = async (loginToken?: string, wxUserInfo?: any) => { const confirmToken = loginToken || token; if (!confirmToken) { @@ -113,19 +163,27 @@ const QRConfirmPage: React.FC = () => { return; } - if (!user?.userId) { - setError('请先登录小程序'); - Taro.showToast({ - title: '请先登录小程序', - icon: 'none' - }); - - setTimeout(() => { - Taro.redirectTo({ - url: '/passport/login' + // 优先使用传入的用户信息,否则尝试从本地存储获取 + const currentUser = wxUserInfo || userInfo; + + if (!currentUser?.userId) { + // 没有用户信息,尝试从本地存储获取 + const userId = Taro.getStorageSync('UserId'); + if (!userId) { + setError('请先登录小程序'); + Taro.showToast({ + title: '请先登录小程序', + icon: 'none' }); - }, 1500); - return; + + setTimeout(() => { + Taro.switchTab({ + url: '/pages/user/user' + }); + }, 1500); + return; + } + currentUser && (currentUser.userId = Number(userId)); } try { @@ -134,11 +192,11 @@ const QRConfirmPage: React.FC = () => { const result = await confirmQRLogin({ token: confirmToken, - userId: user.userId, + userId: currentUser.userId, platform: 'wechat', wechatInfo: { - nickname: user.nickname, - avatar: user.avatar + nickname: currentUser.nickname || currentUser.username, + avatar: currentUser.avatar } }); @@ -324,7 +382,8 @@ const QRConfirmPage: React.FC = () => { if (loginMethod === 'url') { return '检测到登录请求,是否确认登录?'; } - return `确认使用 ${getDisplayName()} 登录网页端?`; + const displayName = userInfo?.nickname || userInfo?.username || '当前用户'; + return `确认使用 ${displayName} 登录网页端?`; }; return ( @@ -355,13 +414,13 @@ const QRConfirmPage: React.FC = () => { {/* 用户信息 */} - {!loading && !confirmed && !error && user && ( + {!loading && !confirmed && !error && userInfo && ( - {user.avatar ? ( + {userInfo.avatar ? ( ) : ( @@ -370,10 +429,10 @@ const QRConfirmPage: React.FC = () => { )} - {user.nickname || user.username || '用户'} + {userInfo.nickname || userInfo.username || '用户'} - ID: {user.userId} + ID: {userInfo.userId}