You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

NextJS 13路由控制:仅允许通过应用内按钮跳转路由

解决NextJS 13 + NestJS路由控制的数据库同步延迟问题

你的核心问题是前端路由跳转与后端数据库更新的时序不匹配:线上环境数据库操作存在延迟,导致Middleware校验时还没读到最新的页面状态,直接把跳转请求重定向回去了。下面给出几种可落地的解决方案,按优先级排序:

方案一:等待数据库更新完成后再跳转(最直接有效)

问题根源是你调用异步的useSetCurrentPage后立刻执行router.push,没等数据库更新完成。只需修改按钮逻辑,确保后端确认页面更新后再触发路由跳转。

修改按钮点击逻辑

// 原错误代码(未等待异步操作完成)
// handlenextPage{ useSetCurrentPage("/battle/step2"); router.push("/battle/step2")} 

// 修改后的正确逻辑
const handleNextPage = async () => {
  await useSetCurrentPage("/battle/step2"); // 等待数据库更新成功
  router.push("/battle/step2");
};

为什么有效

线上环境的数据库操作(尤其是跨区域部署或有读写分离的场景)会有网络延迟,await会强制前端等待后端返回更新成功的响应,此时Middleware查询数据库时能拿到最新的页面状态,校验自然通过。

方案二:优化Middleware,应对数据库延迟

如果方案一仍存在极端场景的延迟(比如主从同步延迟),可以给Middleware增加重试机制,或者强制读取主库数据。

2.1 增加数据库查询重试

给getCurrentPageInDb加最多2次重试,每次间隔100ms,覆盖短暂的同步延迟:

import { getCurrentPageInDb } from "./services/currentPage/currentPage.service";
import { NextRequest, NextResponse } from "next/server";
import { parseCookie } from "utils";

const MAX_RETRIES = 2;
const RETRY_DELAY = 100; // 延迟100ms再重试

// 带重试的页面查询函数
const getCurrentPageWithRetry = async (accessToken: string, retries: number): Promise<string | null> => {
  try {
    return await getCurrentPageInDb(accessToken);
  } catch (err) {
    if (retries > 0) {
      await new Promise(resolve => setTimeout(resolve, RETRY_DELAY));
      return getCurrentPageWithRetry(accessToken, retries - 1);
    }
    throw err;
  }
};

export default async function middleware(req: NextRequest) {
  const cookieHeader = req.headers.get("cookie");
  if (!cookieHeader) return NextResponse.redirect(new URL("/", req.url));

  const cookies = parseCookie(cookieHeader);
  const accessToken = cookies["access-token"];
  if (!accessToken) return NextResponse.redirect(new URL("/", req.url));

  let currentPage;
  try {
    currentPage = await getCurrentPageWithRetry(accessToken, MAX_RETRIES);
  } catch (err) {
    console.error("获取当前页面失败:", err);
    return NextResponse.redirect(new URL("/", req.url));
  }

  if (currentPage && req.nextUrl.pathname !== currentPage) {
    return NextResponse.redirect(new URL(currentPage, req.url));
  }

  return NextResponse.next();
}

export const config = {
  matcher: ["/battle/:path*", "/admin/:path*"],
};

2.2 强制读取主库(针对读写分离场景)

如果后端用了主从数据库,默认查询可能走从库,导致更新后无法立即读到最新数据。修改NestJS的getCurrentPageInDb服务,强制查询主库:

// NestJS服务示例(以TypeORM为例)
async getCurrentPageInDb(accessToken: string): Promise<string | null> {
  // 指定使用主库连接查询
  const user = await this.userRepository.query(
    'SELECT current_page FROM users WHERE access_token = $1',
    [accessToken],
    { connection: 'master' } // 替换为你的主库连接名
  );
  return user?.[0]?.current_page || null;
}

方案三:前端状态配合后端校验(双重保障)

在前端维护当前页的全局状态,先拦截非法跳转,同时保留Middleware作为最终校验层,减少后端请求压力。

3.1 创建全局页面状态Context

// contexts/CurrentPageContext.tsx
'use client';

import { createContext, useContext, useState, useEffect } from 'react';
import { useRouter } from 'next/navigation';
import { useSetCurrentPage } from '@/hooks/useSetCurrentPage';

const CurrentPageContext = createContext<{
  currentPage: string | null;
  navigateToNextPage: (page: string) => Promise<void>;
}>({
  currentPage: null,
  navigateToNextPage: async () => {},
});

export const CurrentPageProvider = ({ children }: { children: React.ReactNode }) => {
  const [currentPage, setCurrentPageState] = useState<string | null>(null);
  const router = useRouter();
  const updatePage = useSetCurrentPage;

  // 页面加载时从后端获取初始页面
  useEffect(() => {
    const fetchInitialPage = async () => {
      const accessToken = document.cookie.split('; ').find(row => row.startsWith('access-token'))?.split('=')[1];
      if (!accessToken) return;
      
      const res = await fetch('/api/current-page', {
        headers: { 'Authorization': `Bearer ${accessToken}` }
      });
      const data = await res.json();
      setCurrentPageState(data.currentPage);
    };
    fetchInitialPage();
  }, []);

  // 统一处理跳转逻辑:更新数据库 → 更新前端状态 → 跳转路由
  const navigateToNextPage = async (page: string) => {
    await updatePage(page);
    setCurrentPageState(page);
    router.push(page);
  };

  return (
    <CurrentPageContext.Provider value={{ currentPage, navigateToNextPage }}>
      {children}
    </CurrentPageContext.Provider>
  );
};

export const useCurrentPage = () => useContext(CurrentPageContext);

3.2 在页面组件中使用Context

'use client';

import { useCurrentPage } from '@/contexts/CurrentPageContext';

const Step1Page = () => {
  const { navigateToNextPage } = useCurrentPage();

  const handleNext = () => {
    navigateToNextPage("/battle/step2");
  };

  return (
    <button onClick={handleNext}>进入第二步</button>
  );
};

export default Step1Page;

3.3 前端拦截非法路由(可选)

用beforePopState拦截浏览器前进后退或手动修改URL的行为:

// 在CurrentPageProvider的useEffect中添加
useEffect(() => {
  const unsubscribe = router.beforePopState(({ url }) => {
    const targetPath = new URL(url, window.location.origin).pathname;
    if (currentPage && targetPath !== currentPage) {
      router.push(currentPage);
      return false; // 阻止默认跳转
    }
    return true;
  });
  return () => unsubscribe();
}, [currentPage, router]);

方案四:Cookie同步页面状态(减少数据库依赖)

除了数据库存储,同时将当前页写入Cookie,Middleware优先校验Cookie,再同步数据库状态,避免频繁查询数据库。

4.1 修改前端页面更新逻辑

export const useSetCurrentPage = async (currentPageToken: string) => {
  const accessToken = document.cookie.split('; ').find(row => row.startsWith('access-token'))?.split('=')[1];
  if (!accessToken) {
    console.error('未找到access-token');
    return;
  }

  // 先设置Cookie,立即生效
  document.cookie = `current-page=${currentPageToken}; path=/; secure; same-site=strict`;
  
  // 再更新数据库
  await updateCurrentPageInDb(accessToken, currentPageToken);
};

4.2 修改Middleware校验逻辑

export default async function middleware(req: NextRequest) {
  const cookieHeader = req.headers.get("cookie");
  if (!cookieHeader) return NextResponse.redirect(new URL("/", req.url));

  const cookies = parseCookie(cookieHeader);
  const accessToken = cookies["access-token"];
  const currentPageFromCookie = cookies["current-page"];

  if (!accessToken) return NextResponse.redirect(new URL("/", req.url));

  // 优先用Cookie校验,避免数据库查询延迟
  if (currentPageFromCookie && req.nextUrl.pathname !== currentPageFromCookie) {
    return NextResponse.redirect(new URL(currentPageFromCookie, req.url));
  }

  // Cookie不存在时,查询数据库并同步Cookie
  const currentPage = await getCurrentPageInDb(accessToken);
  if (currentPage) {
    const response = NextResponse.next();
    response.cookies.set("current-page", currentPage, {
      path: "/",
      secure: true,
      sameSite: "strict"
    });
    if (req.nextUrl.pathname !== currentPage) {
      return NextResponse.redirect(new URL(currentPage, req.url));
    }
    return response;
  }

  return NextResponse.next();
}

注意:Cookie可能被用户篡改,所以关键操作(比如提交表单)仍需校验数据库中的真实页面状态,避免安全风险。

内容的提问来源于stack exchange,提问作者Botnari Ion

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.17 11:45:00