R69 完成业务验收与会话稳定性修复

这个提交包含在:
2026-04-26 21:02:00 +08:00
父节点 63acc2db92
当前提交 87316044d1
共修改 15 个文件,包含 158 行新增和 21 行删除
+3
查看文件
@@ -24,6 +24,7 @@ import {
loginWithPassword,
logoutSession,
refreshSession,
resetRefreshSessionCache,
} from "./authService";
import { resolvePermissionsForRoles } from "./permissions";
@@ -246,6 +247,7 @@ export function AuthProvider({ children }: PropsWithChildren) {
}, [clearProactiveRefreshTimer]);
const clearSession = useCallback(() => {
resetRefreshSessionCache();
clearSessionHint();
markUnauthenticatedReady();
void clearPwaRuntimeCaches();
@@ -427,6 +429,7 @@ export function AuthProvider({ children }: PropsWithChildren) {
const changePassword = useCallback(
async (oldPassword: string, newPassword: string) => {
const result = await changePasswordApi({ oldPassword, newPassword });
resetRefreshSessionCache();
const me = await fetchCurrentEmployee(result.accessToken);
commitAuthenticatedUser(result.accessToken, me, false);
scheduleProactiveRefresh(result.expiresIn);
+61 -6
查看文件
@@ -6,7 +6,20 @@ const JSON_HEADERS = {
"content-type": "application/json",
};
const REFRESH_SUCCESS_REPLAY_MS = 1000;
let refreshSessionPromise: Promise<LoginResponse> | null = null;
let refreshSessionReplay: { data: LoginResponse; expiresAt: number } | null = null;
let refreshSessionGeneration = 0;
export function resetRefreshSessionCache() {
refreshSessionGeneration += 1;
refreshSessionPromise = null;
refreshSessionReplay = null;
}
export async function loginWithPassword(payload: LoginRequest) {
resetRefreshSessionCache();
return apiRequestData<LoginResponse>("/api/auth/login", {
method: "POST",
auth: false,
@@ -19,7 +32,7 @@ export async function loginWithPassword(payload: LoginRequest) {
});
}
export async function refreshSession() {
async function requestRefreshSession() {
return apiRequestData<LoginResponse>("/api/auth/refresh", {
method: "POST",
auth: false,
@@ -30,12 +43,54 @@ export async function refreshSession() {
});
}
export async function refreshSession() {
const now = Date.now();
if (refreshSessionReplay && refreshSessionReplay.expiresAt > now) {
return refreshSessionReplay.data;
}
if (!refreshSessionPromise) {
const generation = refreshSessionGeneration;
const requestPromise = requestRefreshSession()
.then((data) => {
if (generation === refreshSessionGeneration) {
refreshSessionReplay = {
data,
expiresAt: Date.now() + REFRESH_SUCCESS_REPLAY_MS,
};
}
return data;
})
.catch((error: unknown) => {
if (generation === refreshSessionGeneration) {
refreshSessionReplay = null;
}
throw error;
});
refreshSessionPromise = requestPromise;
void requestPromise
.finally(() => {
if (refreshSessionPromise === requestPromise) {
refreshSessionPromise = null;
}
})
.catch(() => undefined);
}
return refreshSessionPromise;
}
export async function logoutSession() {
return apiRequestData<Record<string, never>>("/api/auth/logout", {
method: "POST",
auth: false,
retryOn401: false,
});
try {
return await apiRequestData<Record<string, never>>("/api/auth/logout", {
method: "POST",
auth: false,
retryOn401: false,
});
} finally {
resetRefreshSessionCache();
}
}
export async function fetchCurrentEmployee(accessToken: string) {
+2 -2
查看文件
@@ -253,7 +253,7 @@ export function AssetRegistryPage() {
}, [isCreatingAsset, searchParams, selectedAssetId, setSearchParams]);
useEffect(() => {
if (!assetDetailQuery.data?.asset) {
if (isCreatingAsset || !assetDetailQuery.data?.asset) {
return;
}
const asset = assetDetailQuery.data.asset;
@@ -275,7 +275,7 @@ export function AssetRegistryPage() {
setMovementNote("");
setMovementError("");
setAssetFormError("");
}, [assetDetailQuery.data]);
}, [assetDetailQuery.data, isCreatingAsset]);
const filteredAssetCostCenters = useMemo(() => {
if (!assetForm.legalEntityId) {
+7 -1
查看文件
@@ -228,7 +228,7 @@ export function MobileApprovalsPage() {
const [transferToApproverId, setTransferToApproverId] = useState("");
const [transferReason, setTransferReason] = useState("");
const [offlineDraftSubmitting, setOfflineDraftSubmitting] = useState(false);
const [isOnline, setIsOnline] = useState<boolean>(typeof navigator === "undefined" ? true : navigator.onLine);
const [isOnline, setIsOnline] = useState(true);
const autoSyncRequestedRef = useRef(false);
const ownerEmployeeId = me?.id ?? null;
@@ -673,6 +673,12 @@ export function MobileApprovalsPage() {
};
}, []);
useEffect(() => {
if (accessToken) {
setIsOnline(true);
}
}, [accessToken]);
useEffect(() => {
if (!autoSyncRequestedRef.current || !canUseRemote || syncMutation.isPending) {
return;