refactor(auth): centralize login status sync

This commit is contained in:
2026-06-18 12:52:49 +08:00
parent 0d0dabaace
commit f600e11d55
11 changed files with 115 additions and 128 deletions
+1 -1
View File
@@ -13,7 +13,7 @@ export interface AuthState {
username: string;
confirmPassword: string;
errorMessage: string | null;
/** 当前登录状态(未登录 / 游客 / OAuth / 邮箱) */
/** 当前登录状态(未登录 / 游客 / OAuth / 邮箱)—— 下游同步器监听此字段的双向变化 */
loginStatus: LoginStatus;
/** 启动时的本地登录态恢复是否已经完成 */
hasInitialized: boolean;
+72
View File
@@ -0,0 +1,72 @@
"use client";
/**
* Auth → Chat 同步器
*
* Chat machine 不直接感知 Auth machine;这里作为根级桥接器,持续监听
* loginStatus 变化并派发 chat 鉴权生命周期事件。这样页面组件只负责 UI,
* 不再承担全局状态同步职责。
*/
import { useEffect, useRef } from "react";
import { usePathname, useRouter } from "next/navigation";
import type { LoginStatus } from "@/data/dto/auth";
import { AuthStorage } from "@/data/storage/auth/auth_storage";
import { useAuthState } from "@/stores/auth/auth-context";
import { PROTECTED_ROUTES, ROUTES, type StaticRoute } from "@/router/routes";
import { useChatDispatch } from "./chat-context";
function isProtectedPath(pathname: string): boolean {
return PROTECTED_ROUTES.some((route: StaticRoute) => pathname === route);
}
export function ChatAuthSync() {
const authState = useAuthState();
const chatDispatch = useChatDispatch();
const pathname = usePathname();
const router = useRouter();
const prevLoginStatusRef = useRef<LoginStatus | null>(null);
useEffect(() => {
if (!authState.hasInitialized || authState.isLoading) return;
const prev = prevLoginStatusRef.current;
if (prev === authState.loginStatus) return;
prevLoginStatusRef.current = authState.loginStatus;
if (authState.loginStatus === "notLoggedIn") {
chatDispatch({ type: "ChatLogout" });
if (isProtectedPath(pathname)) {
router.replace(ROUTES.splash);
}
return;
}
if (authState.loginStatus === "guest") {
chatDispatch({ type: "ChatGuestLogin" });
return;
}
let cancelled = false;
void (async () => {
const tokenR = await AuthStorage.getInstance().getLoginToken();
if (!cancelled && tokenR.success && tokenR.data) {
chatDispatch({ type: "ChatNonGuestLogin", token: tokenR.data });
}
})();
return () => {
cancelled = true;
};
}, [
authState.hasInitialized,
authState.isLoading,
authState.loginStatus,
chatDispatch,
pathname,
router,
]);
return null;
}
+2 -2
View File
@@ -4,7 +4,7 @@
* 事件名与原 Dart ChatEvent 对齐。
*
* 历史:原本有 `ChatAuthStatusChanged`chat 机器刷新 isGuest)—— 已删。
* 鉴权状态变化(loginStatus)由 chat-screen 通过 `useAuthState()` 订阅,
* 鉴权状态变化(loginStatus)由 <ChatAuthSync /> 通过 `useAuthState()` 订阅,
* chat 机器不感知鉴权。
*
* 鉴权生命周期事件(本轮新增):
@@ -16,7 +16,7 @@
* chat-screen 不直接创建 ChatWebSocket / 不读 AuthStorage(除 token 取值)。
*/
export type ChatEvent =
// 鉴权生命周期(用户显式触发登录后由 chat-screen 派)
// 鉴权生命周期(登录态变化后由 ChatAuthSync 派)
| { type: "ChatGuestLogin" }
| { type: "ChatNonGuestLogin"; token: string }
| { type: "ChatLogout" }
+12 -6
View File
@@ -2,8 +2,8 @@
* Chat 状态机(XState v5
*
* 鉴权解耦(事件驱动):
* chat 机器不感知鉴权 / 不管 WebSocket —— 由 chat-screen 派生 loginStatus
* chat-screen 只派 3 个事件:
* chat 机器不感知鉴权 / 不管 WebSocket —— 由 <ChatAuthSync /> 派生 loginStatus
* ChatAuthSync 只派 3 个事件:
* - `ChatGuestLogin` → 游客会话(断 WS = 不连)
* - `ChatNonGuestLogin { token }` → 非游客会话(连 WS)
* - `ChatLogout` → 登出(机器自动 cleanup WS actor
@@ -221,6 +221,10 @@ export const chatMachine = setup({
// guestSession(游客会话 —— 不连 WS,2 个 init 任务并行)
// ========================================================================
guestSession: {
on: {
ChatLogout: "#chat.idle",
ChatNonGuestLogin: "#chat.userSession",
},
initial: "initializing",
states: {
initializing: {
@@ -311,8 +315,6 @@ export const chatMachine = setup({
},
],
// 删除 ChatLoadMoreHistory handler —— 游客无服务端 history,不支持翻页
ChatLogout: "#chat.idle",
ChatNonGuestLogin: "#chat.userSession",
ChatAISentenceReceived: { actions: "appendOrUpdateAISentence" },
ChatWebSocketError: { actions: "appendSocketErrorMessage" },
ChatWebSocketConnected: { actions: "setWsConnected" },
@@ -355,6 +357,12 @@ export const chatMachine = setup({
// - sendingViaWs 重新声明 `ChatWebSocketError` 加 target: "ready"(带 action
// —— XState v5: child handler 替换 parent;复制 action
on: {
ChatLogout: "#chat.idle",
ChatGuestLogin: "#chat.guestSession",
ChatNonGuestLogin: {
target: "#chat.userSession",
reenter: true,
},
ChatAISentenceReceived: { actions: "appendOrUpdateAISentence" },
ChatWebSocketError: { actions: "appendSocketErrorMessage" },
ChatWebSocketConnected: { actions: "setWsConnected" },
@@ -424,8 +432,6 @@ export const chatMachine = setup({
ChatLoadMoreHistory: {
target: "loadingMore",
},
ChatLogout: "#chat.idle",
ChatGuestLogin: "#chat.guestSession",
ChatQuotaExceeded: { actions: "incrementQuotaExceeded" },
// 注:ChatAISentenceReceived / ChatWebSocketError / ChatWebSocketConnected
// 已上提到 userSession.on,子级不再声明
+1
View File
@@ -2,6 +2,7 @@
* @file Automatically generated by barrelsby.
*/
export * from "./chat-auth-sync";
export * from "./chat-context";
export * from "./chat-events";
export * from "./chat-machine.actors";
+17 -13
View File
@@ -1,35 +1,39 @@
"use client";
/**
* Auth → User 启动同步器
* Auth → User 同步器
*
* AuthStatusChecker 只负责恢复 loginStatus头像等登录用户资料由
* UserStorage 持久化在 user machine 这一侧。冷启动直达 /chat 时,
* 没有经过 splash/auth 的跳转副作用,因此需要在登录态恢复后主动
* hydrate user store
* AuthStatusChecker 只负责恢复 loginStatus;用户资料由 user machine
* 管理。这里持续监听登录态变化:
* - notLoggedIn → 清空本地 user context
* - guest / email / OAuth → 重新 hydrate user store
*/
import { useEffect, useRef } from "react";
import type { LoginStatus } from "@/data/dto/auth";
import { useAuthState } from "@/stores/auth/auth-context";
import { useUserDispatch } from "./user-context";
export function UserAuthSync() {
const { loginStatus } = useAuthState();
const { hasInitialized, isLoading, loginStatus } = useAuthState();
const userDispatch = useUserDispatch();
const lastInitializedStatusRef = useRef<string | null>(null);
const prevLoginStatusRef = useRef<LoginStatus | null>(null);
//TODO 在测位栏界面监听变化,监听登录状态的变化若发生变化,则需要分发 user init 事件
useEffect(() => {
if (loginStatus === "notLoggedIn" || loginStatus === "guest") {
lastInitializedStatusRef.current = null;
if (!hasInitialized || isLoading) return;
const prev = prevLoginStatusRef.current;
if (prev === loginStatus) return;
prevLoginStatusRef.current = loginStatus;
if (loginStatus === "notLoggedIn") {
userDispatch({ type: "UserClearLocal" });
return;
}
if (lastInitializedStatusRef.current === loginStatus) return;
lastInitializedStatusRef.current = loginStatus;
userDispatch({ type: "UserInit" });
}, [loginStatus, userDispatch]);
}, [hasInitialized, isLoading, loginStatus, userDispatch]);
return null;
}