轻松聘
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

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,
};
}