You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
142 lines
5.1 KiB
142 lines
5.1 KiB
import { useCallback, useState } from 'react';
|
|
import { fetchWechatLoginResult } from '../api';
|
|
import type { AppRole, WechatPendingTicket, WechatUserProfile } from '../types';
|
|
import { isWechatAvailable, requestWechatAuthCode } from '../lib/wechat-sdk';
|
|
import { useAuth } from '../context/AuthContext';
|
|
|
|
/* ===================== 微信登录状态机 =====================
|
|
* 编排三种账号场景的流转,页面只按 state.kind 渲染,不写判断逻辑。
|
|
*
|
|
* idle ──start()──> 本地 token 有效?──是──> alreadyLoggedIn(免授权直接落地)
|
|
* │
|
|
* 否
|
|
* ↓
|
|
* authenticating(调起微信)
|
|
* ↓
|
|
* verifying(后端校验 code)
|
|
* ↓
|
|
* ┌───────────────┼───────────────┐
|
|
* new 分支 active 分支 deleted 分支
|
|
* ↓ ↓ ↓
|
|
* needBindPhone success deleted(提示无法登录)
|
|
* (跳手机号+验证码) │
|
|
* ↓ cancelLogin() → idle
|
|
* 由 P2 页完成绑定后落地
|
|
*
|
|
* 注意:注销审核期内的账号不可登录、不可复活,因此 deleted 分支没有「重新注册」出口。
|
|
*/
|
|
|
|
/** 待绑定手机号时持有的临时凭证(原 code 已失效,只能用它标识身份) */
|
|
export type PendingBindPhone = WechatPendingTicket;
|
|
|
|
/** 微信登录状态(判别联合,页面通过 kind 收窄后读取对应字段) */
|
|
export type WechatLoginState =
|
|
| { kind: 'idle' }
|
|
/** 正在调起微信 */
|
|
| { kind: 'authenticating' }
|
|
/** 已拿到 code,正在请求后端校验 */
|
|
| { kind: 'verifying' }
|
|
/** 本地 token 未过期,无需再走微信授权 */
|
|
| { kind: 'alreadyLoggedIn' }
|
|
/** 第一次登录:openid 未绑定账号,需跳手机号 + 验证码完成注册 */
|
|
| { kind: 'needBindPhone'; ticket: PendingBindPhone }
|
|
/** 账号处于注销审核期,不可登录 */
|
|
| { kind: 'deleted' }
|
|
/** 登录成功;role 为 undefined 表示尚未选择身份,需引导去注册流程 */
|
|
| { kind: 'success'; profile: WechatUserProfile; role?: AppRole }
|
|
/** 失败(未安装微信 / 用户取消 / 网络异常) */
|
|
| { kind: 'error'; message: string };
|
|
|
|
export interface UseWechatLoginReturn {
|
|
state: WechatLoginState;
|
|
/** 是否处于请求中(用于按钮 loading 与防重复点击) */
|
|
loading: boolean;
|
|
/** 入口:本地 token 有效则直接登录,否则走微信授权 */
|
|
start: () => Promise<void>;
|
|
/** 关闭注销提示,回到登录页 */
|
|
cancelLogin: () => void;
|
|
/** 重置为初始态 */
|
|
reset: () => void;
|
|
}
|
|
|
|
/** 初始态 */
|
|
const IDLE: WechatLoginState = { kind: 'idle' };
|
|
|
|
export function useWechatLogin(): UseWechatLoginReturn {
|
|
const { applySession, isTokenValid } = useAuth();
|
|
const [state, setState] = useState<WechatLoginState>(IDLE);
|
|
|
|
const start = useCallback(async () => {
|
|
// 第二次及以后登录:本地令牌仍有效时免微信授权
|
|
if (isTokenValid) {
|
|
setState({ kind: 'alreadyLoggedIn' });
|
|
return;
|
|
}
|
|
|
|
setState({ kind: 'authenticating' });
|
|
try {
|
|
if (!isWechatAvailable()) {
|
|
throw new Error('未检测到微信客户端,请先安装微信');
|
|
}
|
|
const code = await requestWechatAuthCode();
|
|
|
|
setState({ kind: 'verifying' });
|
|
const result = await fetchWechatLoginResult(code);
|
|
|
|
switch (result.status) {
|
|
case 'active': {
|
|
if (!result.profile) {
|
|
throw new Error('登录信息不完整,请重试');
|
|
}
|
|
applySession({
|
|
accessToken: result.token,
|
|
// 微信通道无后端契约,refreshToken 缺失时用空串占位(本通道整体 mock)
|
|
refreshToken: result.refreshToken ?? '',
|
|
expiresAt: result.expiresAt,
|
|
backendRole:
|
|
result.role === 'enterprise'
|
|
? 'COMPANY'
|
|
: result.role === 'candidate'
|
|
? 'STUDENT'
|
|
: undefined,
|
|
});
|
|
setState({ kind: 'success', profile: result.profile, role: result.role });
|
|
break;
|
|
}
|
|
case 'new': {
|
|
setState({
|
|
kind: 'needBindPhone',
|
|
ticket: { token: result.token, openid: result.openid },
|
|
});
|
|
break;
|
|
}
|
|
case 'deleted': {
|
|
// 注销审核期:不写登录态,仅提示不可登录
|
|
setState({ kind: 'deleted' });
|
|
break;
|
|
}
|
|
}
|
|
} catch (e) {
|
|
setState({
|
|
kind: 'error',
|
|
message: e instanceof Error ? e.message : '微信登录失败,请稍后重试',
|
|
});
|
|
}
|
|
}, [isTokenValid, applySession]);
|
|
|
|
const cancelLogin = useCallback(() => {
|
|
setState(IDLE);
|
|
}, []);
|
|
|
|
const reset = useCallback(() => {
|
|
setState(IDLE);
|
|
}, []);
|
|
|
|
return {
|
|
state,
|
|
loading: state.kind === 'authenticating' || state.kind === 'verifying',
|
|
start,
|
|
cancelLogin,
|
|
reset,
|
|
};
|
|
}
|
|
|