ProductReadyProductReady

路由

学习如何在 ProductReady 中使用 Next.js App Router、tRPC、oRPC OpenAPI 和 Wouter 创建和管理路由。

概述

ProductReady 采用混合路由架构,结合了 Next.js App Router(服务端渲染)、tRPC(类型安全 API)、oRPC + OpenAPI(契约优先 REST API)和 Wouter(仪表板区域的客户端 SPA 路由)。

本指南将帮助您理解何时使用每种路由方式以及如何创建新路由。


主要应用路由

ProductReady 预置了几个开箱即用的路由:

落地页 (/)

路由文件: src/app/[lang]/(home)/page.tsx

营销主页,包含:

  • 英雄区和行动号召
  • 功能亮点
  • 定价信息
  • 响应式设计和国际化支持

访问权限: 公开 - 无需身份验证

使用场景: 营销、潜在客户生成、产品信息展示


仪表盘 (/dashboard)

路由文件: src/app/(dashboard)/dashboard/page.tsx

主应用界面,包含:

  • 任务管理系统
  • 分析和指标
  • 最近活动信息流
  • 快捷操作和快捷方式
  • 使用 Wouter 进行客户端 SPA 路由

访问权限: 受保护 - 需要身份验证(或使用 ?demo=1 演示模式)

使用场景: 主要用户工作区、任务跟踪、数据可视化


智能体任务 (/agent)

路由文件: src/app/(dashboard)/agent/[[...slug]]/page.tsx

AI 智能体任务管理界面,包含:

  • 任务创建和监控
  • 与 AI 智能体的实时流式聊天
  • 支持 Resume Streams 的后台任务执行
  • 产物生成和展示(双列布局)
  • 任务状态跟踪和历史记录

访问权限: 受保护 - 需要身份验证(或使用 ?demo=1 演示模式)

使用场景: AI 驱动的自动化、长时间运行的任务、智能体工作流

关键特性:

  • Resume Streams(恢复流): 即使用户断开连接,任务仍继续运行
  • 多态产物: 生成的输出可以链接到文章和其他实体
  • 后台执行: 任务在服务器端运行,无需用户在场

系统管理 (/systemadmin)

路由文件: src/app/(dashboard)/systemadmin/[[...slug]]/page.tsx

基于角色的访问控制系统管理面板:

  • 用户管理(创建、编辑、删除用户)
  • 系统管理员管理(owner/admin 角色)
  • 系统设置和配置
  • 访问日志和监控
  • 使用 Wouter 进行客户端 SPA 路由

访问权限: 受限 - 需要系统管理员权限(owner 或 admin 角色)

授权机制:

  • 页面级别: 每次页面加载时检查 systemAdmins
  • API 级别: 所有操作使用 siteAdminProcedure 保护
  • 角色层级:
    • Owner(拥有者): 完全控制,可以管理其他管理员
    • Admin(管理员): 可以管理用户但不能管理其他管理员

使用场景: 平台管理、用户配置、安全管理

默认访问: 默认情况下没有用户拥有管理员访问权限。运行 pnpm db:seed 创建 owner 账户(admin@productready.dev)。

密码随机生成并显示在 seed 输出中,或通过 SYSTEM_ADMIN_PASSWORD 环境变量设置。


路由类型

1. Next.js App Router(服务端页面)

适用场景: SEO 关键页面、营销页面、博客文章、文档

位置: src/app/[lang]/(group)/page.tsx

示例:

// src/app/[lang]/(home)/pricing/page.tsx
export default function PricingPage() {
  return (
    <div>
      <h1>定价</h1>
      {/* 带 SEO 元数据的 SSR 内容 */}
    </div>
  );
}

// 可选:为 SEO 添加元数据
export const metadata = {
  title: "定价 - ProductReady",
  description: "选择最适合您的方案",
};

创建新页面:

  1. 在适当的路由组中创建文件:

    • (home) - 公开营销页面
    • (legal) - 条款、隐私政策
    • (dashboard) - 仪表板根页面(内部使用 Wouter)
  2. 导出默认 React 组件

  3. 添加 SEO 元数据(可选)


2. tRPC API 路由(类型安全 API)

适用场景: 内部 API、实时数据获取、数据变更、类型安全的客户端-服务器通信

位置: src/server/routers/*.ts

示例:

后端(路由器):

// src/server/routers/posts.ts
import { z } from "zod";
import { protectedProcedure, publicProcedure, router } from "../trpc";

export const postsRouter = router({
  // Query - 获取数据
  list: publicProcedure
    .input(
      z.object({
        limit: z.number().min(1).max(100).default(10),
        offset: z.number().min(0).default(0),
      })
    )
    .query(async ({ ctx, input }) => {
      const posts = await ctx.db.query.posts.findMany({
        limit: input.limit,
        offset: input.offset,
      });
      return { posts, total: posts.length };
    }),

  // Mutation - 修改数据
  create: protectedProcedure
    .input(
      z.object({
        title: z.string().min(1),
        content: z.string(),
      })
    )
    .mutation(async ({ ctx, input }) => {
      const post = await ctx.db.insert(posts).values({
        userId: ctx.user.id,
        title: input.title,
        content: input.content,
      });
      return post;
    }),
});

注册路由器:

// src/server/routers/index.ts
import { router } from "../trpc";
import { postsRouter } from "./posts";

export const appRouter = router({
  posts: postsRouter,
  // ... 其他路由器
});

前端(客户端):

"use client";
import { api } from "~/lib/trpc/client";

function PostsList() {
  const { data, isLoading } = api.posts.list.useQuery({ limit: 10, offset: 0 });
  
  if (isLoading) return <div>加载中...</div>;
  
  return (
    <div>
      {data?.posts.map((post) => (
        <div key={post.id}>{post.title}</div>
      ))}
    </div>
  );
}

优势:

  • ✅ 完整的 TypeScript 端到端类型安全
  • ✅ 无需手动编写 API 端点
  • ✅ 内置 React Query 集成
  • ✅ 自动请求去重和缓存

3. oRPC OpenAPI 路由(REST API)

适用场景: 公开 REST API、第三方集成、移动应用、Webhook

位置: src/app/api/v1/[[...server]]/app.ts

示例:

// src/domains/openapi/contract.ts
import { oc } from "@orpc/contract";
import { z } from "zod";

export const getUserRoute = oc
  .route({
    method: "GET",
    path: "/api/v1/users/{id}",
    tags: ["Users"],
    summary: "Get user by ID",
  })
  .input(z.object({ params: z.object({ id: z.string() }) }))
  .output(z.object({ id: z.string(), name: z.string(), email: z.string().email() }));

// src/domains/openapi/router.ts
getUser: apiKeyProcedure.users.get.handler(async ({ input }) => {
  const user = await findUserById(input.params.id);
  return toUserVO(user);
});

访问:

  • API 端点:https://yourapp.com/api/v1/users/123
  • OpenAPI 规范:https://yourapp.com/api/v1/openapi.json
  • Swagger UI:https://yourapp.com/api/v1/doc

优势:

  • ✅ 自动生成 OpenAPI 文档
  • ✅ 交互式 Swagger UI 用于测试
  • ✅ Zod schema 验证
  • ✅ 导出 contract,可创建类型安全 TypeScript client
  • ✅ 完美适配外部/公开 API

4. Wouter SPA 路由(客户端路由)

适用场景: 仪表板内部导航、向导流程、选项卡、多步骤表单

位置: src/app/(dashboard)/*/[[...slug]]/page.tsx

示例:

// src/app/(dashboard)/agent/[[...slug]]/page.tsx
"use client";

export const dynamic = "force-dynamic"; // Wouter 必需

import { Route, Router, Switch } from "wouter";
import { DashLink } from "~/components/DashLink";

export default function AgentPage() {
  return (
    <SPAWrapper>
      <Router base="/agent">
        <Switch>
          {/* 列表视图 */}
          <Route path="/">
            <AgentList />
          </Route>
          
          {/* 带 ID 的详情视图 */}
          <Route path="/:id">
            {(params) => <AgentDetail taskId={params.id} />}
          </Route>
          
          {/* 设置 */}
          <Route path="/settings">
            <AgentSettings />
          </Route>
          
          {/* 404 后备 */}
          <Route>
            <NotFound />
          </Route>
        </Switch>
      </Router>
    </SPAWrapper>
  );
}

使用 DashLink 导航:

import { DashLink } from "~/components/DashLink";

function TaskItem({ task }) {
  return (
    <DashLink href={`/${task.id}`}>
      <div>{task.title}</div>
    </DashLink>
  );
}

优势:

  • ✅ 即时的客户端导航(无页面重载)
  • ✅ 自动保留演示模式(?demo=1
  • ✅ 更好的仪表板交互用户体验
  • ✅ 比客户端组件中的 Next.js 路由器更小的 bundle 大小

何时使用 DashLink

  • ✅ SPA 路由内的导航(/agent/*/dashboard/*/kadmin/*
  • ❌ 不要用于跨区段导航(改用 Next.js <Link>

路由示例

示例 1:创建新的公开页面

目标:/features 添加新的"功能"页面

// src/app/[lang]/(home)/features/page.tsx
import { Metadata } from "next";

export const metadata: Metadata = {
  title: "功能 - ProductReady",
  description: "探索 ProductReady 的强大功能",
};

export default function FeaturesPage() {
  return (
    <div className="container mx-auto py-12">
      <h1 className="text-4xl font-bold">功能</h1>
      <p>发现 ProductReady 的特别之处...</p>
    </div>
  );
}

访问: https://yourapp.com/zh-CN/featureshttps://yourapp.com/en/features


示例 2:添加新的 tRPC 端点

目标: 添加一个获取用户统计数据的端点

// src/server/routers/analytics.ts
import { z } from "zod";
import { protectedProcedure, router } from "../trpc";

export const analyticsRouter = router({
  getUserStats: protectedProcedure
    .input(z.object({ userId: z.string() }))
    .query(async ({ ctx, input }) => {
      // 从数据库获取统计数据
      const stats = await ctx.db.query.userStats.findFirst({
        where: eq(userStats.userId, input.userId),
      });
      
      return {
        totalPosts: stats?.totalPosts ?? 0,
        totalViews: stats?.totalViews ?? 0,
        lastActive: stats?.lastActive ?? new Date(),
      };
    }),
});

// 在 src/server/routers/index.ts 中注册
export const appRouter = router({
  analytics: analyticsRouter,
  // ...
});

使用:

"use client";
const { data } = api.analytics.getUserStats.useQuery({ userId: user.id });

示例 3:添加 Wouter SPA 路由

目标: 在 agent 仪表板中添加"历史"选项卡

// src/app/(dashboard)/agent/[[...slug]]/page.tsx
<Router base="/agent">
  <Switch>
    <Route path="/history">
      <AgentHistory />
    </Route>
    {/* ... 其他路由 */}
  </Switch>
</Router>

// 导航
<DashLink href="/history">
  <Button>查看历史</Button>
</DashLink>

决策树:使用哪种路由?

┌─────────────────────────────────────────┐
│   需要 SEO / 服务端渲染吗?              │
└──────────────┬──────────────────────────┘

       ┌───────┴────────┐
       │ 是             │ 否
       ▼                ▼
┌─────────────┐  ┌─────────────────────┐
│  Next.js    │  │  内部/外部?         │
│  App Router │  └──────────┬──────────┘
└─────────────┘             │
  - 营销页面      ┌─────────┴─────────┐
  - 博客          │ 内部              │ 外部
  - 文档          ▼                  ▼
  - 法律文档 ┌──────────────┐  ┌─────────────┐
            │ 需要类型安全?│  │ oRPC OpenAPI│
            └──────┬───────┘  └─────────────┘
                   │            - REST API
         ┌─────────┴────────┐   - Webhook
         │ 是               │ 否│ 移动应用
         ▼                  ▼
    ┌─────────┐      ┌──────────┐
    │  tRPC   │      │  Wouter  │
    └─────────┘      └──────────┘
      - 查询          - 仪表板
      - 变更          - SPA 路由
      - 订阅          - 客户端导航

最佳实践

✅ 应该做的

  • 使用 tRPC 处理所有内部 API 调用(查询、变更)
  • 使用 Wouter 进行仪表板导航以提升用户体验
  • 在 SPA 路由中使用 DashLink 以保留演示模式
  • 为所有公开的 Next.js 页面添加元数据 以优化 SEO
  • 使用路由组 (group) 来逻辑性地组织页面
  • 在 tRPC 和 oRPC OpenAPI 路由中使用 Zod schema 验证输入
  • 在 tRPC 中对需要认证的端点使用 protectedProcedure
  • 为 OpenAPI 路由编写描述和示例文档

❌ 不应该做的

  • 不要在跨区段导航中使用 Wouter(使用 Next.js <Link>
  • 不要在同一组件中混合路由类型(选择一种)
  • 不要忘记 在 Wouter 页面中添加 export const dynamic = "force-dynamic"
  • 不要在 OpenAPI 路由中暴露敏感数据 而不进行身份验证
  • 不要跳过 API 路由中的输入验证
  • 不要硬编码 URL - 为外部服务使用环境变量

文件结构

src/
├── app/
│   ├── [lang]/              # 国际化路由
│   │   ├── (home)/          # 公开页面
│   │   │   ├── page.tsx     # 首页 (/)
│   │   │   ├── pricing/     # 定价页面
│   │   │   └── features/    # 功能页面
│   │   ├── (legal)/         # 法律页面
│   │   │   ├── privacy-policy/
│   │   │   └── terms-of-service/
│   │   ├── docs/            # 文档
│   │   └── blog/            # 博客文章
│   ├── (dashboard)/         # 带 Wouter 的仪表板
│   │   ├── agent/           # Agent SPA 路由
│   │   │   └── [[...slug]]/page.tsx
│   │   ├── dashboard/       # Dashboard SPA 路由
│   │   └── kadmin/          # Admin SPA 路由
│   └── api/                 # API 路由
│       ├── trpc/            # tRPC 端点
│       ├── v1/              # oRPC OpenAPI REST API
│       ├── auth/            # Better Auth
│       └── agent/           # 自定义 API 路由
├── server/
│   └── routers/             # tRPC 路由器
│       ├── index.ts         # 根路由器
│       ├── posts.ts
│       ├── users.ts
│       └── agent-tasks.ts
└── components/
    └── DashLink.tsx         # 带演示模式的 Wouter 链接

环境变量

API 路由所需:

# 数据库
DATABASE_URL="postgresql://..."

# 认证(Better Auth)
BETTER_AUTH_SECRET="your-secret-key"
BETTER_AUTH_URL="http://localhost:3000"

# OpenAI(AI 功能)
OPENAI_API_KEY="sk-..."

# 可选:OpenAPI 路由的外部服务
STRIPE_SECRET_KEY="sk_test_..."

故障排除

问题:Wouter 路由不工作

解决方案: 确保在页面组件顶部添加了 export const dynamic = "force-dynamic"

"use client";
export const dynamic = "force-dynamic"; // ← 添加这行

import { Router } from "wouter";
// ...

问题:tRPC 类型错误

解决方案: 添加新路由器后重新生成类型:

pnpm typecheck

确保您的路由器已在 src/server/routers/index.ts 中注册。


问题:SPA 中演示模式未持久化

解决方案: 使用 DashLink 而不是常规的 <Link><a> 标签:

// ❌ 错误
<Link href="/agent/123">查看任务</Link>

// ✅ 正确
<DashLink href="/123">查看任务</DashLink>

问题:OpenAPI 路由返回 404

解决方案: 检查您是否使用了正确的带 /api/v1 前缀的路径:

# ❌ 错误
curl https://yourapp.com/users/123

# ✅ 正确
curl https://yourapp.com/api/v1/users/123

下一步

  • 认证 - 使用 Better Auth 保护路由
  • 数据库 - 使用 Drizzle ORM 查询数据
  • 测试 - 测试 tRPC 路由器和 API 路由
  • 部署 - 将路由部署到生产环境

其他资源

On this page