路由
学习如何在 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: "选择最适合您的方案",
};创建新页面:
-
在适当的路由组中创建文件:
(home)- 公开营销页面(legal)- 条款、隐私政策(dashboard)- 仪表板根页面(内部使用 Wouter)
-
导出默认 React 组件
-
添加 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/features 或 https://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