MkSaaS 新建项目后先跑通登录,再让订阅状态贯穿整个请求

文章导读
新建 MkSaaS 项目之后,建议先只做一件事:把登录跑通,并确认登录态到底写进了哪里。等这一步能被日志和浏览器面板复现,再把订阅状态的读取点固定到请求入口的中间件里,让业务页面只消费一个已经解析好的状态对象。顺序反了,排查时很容易把“登录没成功”和“订阅查不到”混成同一个问题。
📋 目录
  1. Ⅰ 用脚手架命令生成项目并确认默认页面可访问
  2. Ⅱ 在会话记录与响应头里确认登录态写到了哪里
  3. Ⅲ 把订阅状态的读取点放在业务处理之前
  4. Ⅳ 用一个新账号走完登录到受保护页面的完整路径
  5. Ⅴ 用登录、状态查询、路由判断三段日志定位不一致
A A

新建 MkSaaS 项目之后,建议先只做一件事:把登录跑通,并确认登录态到底写进了哪里。等这一步能被日志和浏览器面板复现,再把订阅状态的读取点固定到请求入口的中间件里,让业务页面只消费一个已经解析好的状态对象。顺序反了,排查时很容易把“登录没成功”和“订阅查不到”混成同一个问题。

先跑通登录、再接入订阅状态,判断依据是登录态能落在 cookie 或会话记录里、订阅状态在中间件阶段被读入并透传给后续处理。适用场景是刚生成项目、只想验证请求链路是否串起来的阶段。操作顺序为:确认默认页面可访问、定位登录写入点、把订阅读取前置到业务处理之前、用新账号走一遍受保护页面。风险边界是各脚手架会话存储方式不同,cookie 名称、字段名、中间件文件名都需要以项目实际配置为准,不能照抄示例。

用脚手架命令生成项目并确认默认页面可访问

先固定一个能复现的起点,后面才不会把端口占用、依赖没装这类环境问题误判成代码问题。生成项目后不要急着改业务代码,先用项目 package.json 里定义的脚本启动,常见是 npm run dev 或 pnpm dev。

  • 监听端口:默认通常是 3000,被占用时脚手理会自动顺延到 3001 之类,以终端输出为准。
  • 默认访问路径:一般是根路径 /,未登录状态下可能直接重定向到 /login 或 /sign-in。
  • 控制台输出:通常会出现类似 ready / Local: http://localhost:3000 的启动行,以及编译完成提示;具体文案随脚手架版本不同,看到可访问地址即可。

用 curl -i http://localhost:3000/ 看一次响应头和状态码,能确认是 200 还是重定向。这一步只是建立一个基线,不做任何登录之外的改动。

在会话记录与响应头里确认登录态写到了哪里

登录成功后,登录态一般通过两种方式落地:会话存储(数据库或内存 session 表)加上一个指向它的 cookie。先找到登录处理入口,它通常在认证路由文件、server action 或认证配置文件中,具体路径取决于脚手架选择。

  • 返回字段:登录响应里常见的字段是 user.id、user.email、expires 一类,实际字段名看项目实现。
  • cookie 名称:可能是 next-auth.session-token、__session 或带项目前缀的 sb-*-auth-token,需要以浏览器面板显示的为准。
  • 查看方式:浏览器开发者工具的 Application → Cookies 看写入项和过期时间;或在命令行用 curl -i 观察 Set-Cookie 响应头。

确认清楚“哪个 cookie 对应哪个用户 id”之后,后面读取订阅状态时才有稳定的关联键。如果这一步字段名没对上,订阅查询会一直查到空值。

MkSaaS 新建项目后先跑通登录,再让订阅状态贯穿整个请求

把订阅状态的读取点放在业务处理之前

订阅状态建议在中间件或请求前置钩子里读一次,而不是在仪表盘、设置页、计费页各查一遍。注册位置通常是项目根目录或 src/ 下的 middleware.ts,注册顺序为:会话解析 → 订阅状态读取 → 路由与业务处理,订阅读取必须排在会话解析之后,因为要用登录态里的用户 id 去查。

下面是一段可替换的骨架,函数名和返回值按项目实际实现改,不要当成某个官方接口:

export async function middleware(req) {
  const session = await getSession(req);        // 第一段:登录态
  if (!session?.user?.id) return redirectToLogin(req);

  const sub = await getSubscription(session.user.id);  // 第二段:订阅状态
  const headers = new Headers(req.headers);
  headers.set('x-user-id', session.user.id);
  headers.set('x-plan', sub?.plan ?? 'free');
  headers.set('x-sub-status', sub?.status ?? 'none');

  return next({ request: { headers } });        // 第三段:透传给路由判断
}

export const config = { matcher: ['/dashboard/:path*', '/settings/:path*'] };

透传方式可以选请求头、请求上下文或者直接挂到 session 对象上,关键是业务代码只读一次解析结果,不再自己发起订阅查询。matcher 决定哪些路径会经过这段逻辑,受保护页面要写进去,否则状态读不到。

MkSaaS 新建项目后先跑通登录,再让订阅状态贯穿整个请求

用一个新账号走完登录到受保护页面的完整路径

测试账号建议新建一个,或使用明确处于免费状态的账号,避免老账号的历史状态干扰判断。走一遍:注册或登录 → 打开受保护路径(例如 /dashboard)→ 观察跳转结果。

  • 预期跳转:如果状态读取正确,免费账号访问收费功能通常会被引导到升级页或计费页,而不是直接进入页面。
  • 日志中应出现的状态值:订阅查询那一段应打印出 plan=free、status=none 或 trialing 之类的值,以及对应的用户 id。
  • 如果日志显示查询到了状态,但页面仍然放行,问题多半出在路由判断那一步没有读到透传值。

这一步不是验证“有没有读”,而是验证“读出来的状态有没有参与判断”。只读到不判断,等于没接。

用登录、状态查询、路由判断三段日志定位不一致

状态对不上时,不要从页面往前猜,按固定顺序看三段日志:

  1. 登录段:打印 userId、会话标识、cookie 名、过期时间。
  2. 状态查询段:打印查询用的 userId、返回的 plan、status、currentPeriodEnd、是否命中缓存。
  3. 路由判断段:打印请求路径、要求的权限级别、最终允许或拒绝的原因。

对比方法是用同一个 requestId 把三段串起来,先确认登录段的 id 与状态查询段使用的 id 是否一致,再确认路由判断段读到的 plan 是否等于状态查询段的返回值。

常见的不一致有三种表现:一是状态查询段的用户 id 为空或与登录段不同,通常是 cookie 名写错或读取时机早于会话写入;二是状态查询命中了旧缓存,导致升级或到期后判断结果没有跟着变;三是部分路径没被 matcher 覆盖,压根没经过状态读取,页面只能拿到默认值。三种情况都能从这三段日志里直接看出来,不需要改代码去试。