# 更新日志 Source: https://codofly.mintlify.app/changelog Codofly 版本更新历史记录 # 更新日志 记录 Codofly 的版本更新历史。 *** ## v1.4.0 (2025-06-01) ### 🚀 新功能 * 新增推荐有奖模块,用户推荐新用户注册可获得奖金 ## v1.3.0 (2025-05-20) ### 🚀 新功能 * 新增 Multimodal 多模态支持,现可上传图片进行分析和处理 * 新增微信支付和支付宝支付渠道 * 新增 AI 插件系统,支持第三方工具集成 ### 🐛 问题修复 * 修复大文件上传时出现的内存溢出问题 * 修复某些 Unicode 字符在流式响应中显示异常的问题 * 修复团队管理页面权限设置不正确的问题 * 修复积分充值后余额显示延迟的问题 ### 💡 改进优化 * 大幅优化流式响应性能,减少首字延迟 50% * 改进代码块渲染,支持更多编程语言高亮 * 改进错误提示系统,提供更明确的解决方案 * 改进用户引导流程,增加交互式教程 *** ## v1.2.0 (2025-04-20) ### 🚀 新功能 * 新增 GPT-4o 和 Gemini 2.0 Pro 模型支持 * 新增自定义模型参数设置(温度、top-p、最大长度等) * 新增 API 密钥管理功能,支持第三方应用集成 * 新增消息标记和收藏功能 * 新增定时任务和自动化工作流支持 ### 🐛 问题修复 * 修复长时间对话导致性能下降的问题 * 修复团队成员删除后聊天记录访问权限异常 * 修复订阅状态更新延迟的问题 * 修复国际化切换时部分文本未翻译的问题 ### 💡 改进优化 * 全面优化前端性能,页面加载速度提升 40% * 改进聊天历史搜索功能,支持语义搜索 * 优化大型对话上下文处理,提高长对话质量 *** ## v1.1.0 (2025-03-20) ### 🚀 新功能 * 新增 Claude 3.7 和 Claude 3.5 模型支持 * 新增聊天会话标签管理功能 * 新增用户偏好设置面板 ### 🐛 问题修复 * 修复流式响应在某些情况下断开的问题 * 修复移动端布局在某些设备上显示异常的问题 * 修复积分计算精度问题 ### 💡 改进优化 * 改进错误提示信息的可读性 * 优化大型聊天记录的加载性能 * 改进国际化体验,支持更多区域设置 * 提升密码安全策略,增加二次验证选项 *** ## v1.0.1 (2024-10-10) ### 🐛 问题修复 * 修复首次登录后重定向问题 * 修复积分购买成功但余额未更新的问题 * 修复部分情况下无法删除聊天记录的问题 * 修复移动设备上键盘弹出时的布局错位 ### 💡 改进优化 * 优化首屏加载速度 * 改进移动端响应式设计 * 增强错误处理和日志记录 * 改进用户引导提示 *** ## v1.0.0 (2024-10-01) ### 🚀 新功能 * 多 AI 模型集成支持 * OpenAI (GPT-4o、GPT-4.1) * Anthropic (Claude 3 系列) * Google (Gemini 2.0、1.5) * xAI (Grok 3、Grok 2) * 完整的订阅和支付系统 * Stripe 订阅管理 * 积分消费系统 * 按模型用量计费 * 企业级团队功能 * 团队创建和管理 * 成员邀请和权限控制 * 资源共享机制 * 用户认证系统 * 社交账号登录 (GitHub, Google) * 邮箱验证和登录 * JWT 会话管理 * 国际化支持 * 英文和简体中文 * 路由级别国际化 * UI 和内容翻译 * 聊天会话管理 * 创建和管理多个会话 * 实时保存聊天记录 * 会话分享功能 * 流式响应系统 * 实时显示 AI 回复 * 支持打字机效果 * 可中断的响应流 * 用户管理面板 * 个人资料管理 * 订阅和账单查看 * 使用统计和分析 ### 💡 改进优化 * 高性能流式响应实现 * 模块化的项目结构 * 完善的错误处理机制 * 企业级安全实践 * 详细的开发文档 * 优雅的响应式设计 * 深色/浅色主题支持 * 无障碍设计原则遵循 *** # API 开发 Source: https://codofly.mintlify.app/development/api 学习如何在 Codofly Template 中创建和管理 API 端点 # API 开发 本文档将指导您如何在 Codofly Template 中创建 API 端点。 ## API Routes 基础 ### 目录结构 ``` app/api/ ├── users/ │ ├── route.ts # GET/POST /api/users │ └── [id]/ │ └── route.ts # GET/PUT/DELETE /api/users/[id] ├── chats/ │ └── route.ts # GET/POST /api/chats └── auth/ └── login/ └── route.ts # POST /api/auth/login ``` 只有 `route.ts` 文件会创建 API 端点,每个 HTTP 方法对应一个导出函数。 ## 创建 API 端点 ### 基础示例 ```typescript title="app/api/users/route.ts" theme={null} import { NextRequest, NextResponse } from 'next/server' import { prisma } from '@/lib/prisma' // GET /api/users export async function GET() { const users = await prisma.user.findMany() return NextResponse.json({ data: users }) } // POST /api/users export async function POST(request: NextRequest) { const body = await request.json() const user = await prisma.user.create({ data: body }) return NextResponse.json({ data: user }, { status: 201 }) } ``` ### 动态路由 ```typescript title="app/api/users/[id]/route.ts" theme={null} export async function GET( request: NextRequest, { params }: { params: { id: string } } ) { const user = await prisma.user.findUnique({ where: { id: params.id } }) if (!user) { return NextResponse.json({ error: '用户未找到' }, { status: 404 }) } return NextResponse.json({ data: user }) } export async function PUT( request: NextRequest, { params }: { params: { id: string } } ) { const body = await request.json() const user = await prisma.user.update({ where: { id: params.id }, data: body }) return NextResponse.json({ data: user }) } export async function DELETE( request: NextRequest, { params }: { params: { id: string } } ) { await prisma.user.delete({ where: { id: params.id } }) return NextResponse.json({ success: true }) } ``` ## 数据库操作 ### Prisma 基础用法 ```typescript title="lib/db-operations.ts" theme={null} import { prisma } from '@/lib/prisma' // 查询操作 export async function getUsers() { return await prisma.user.findMany({ include: { chats: true } }) } // 创建操作 export async function createUser(data: any) { return await prisma.user.create({ data: { email: data.email, name: data.name } }) } // 更新操作 export async function updateUser(id: string, data: any) { return await prisma.user.update({ where: { id }, data }) } ``` ## 认证中间件 ### 保护 API 路由 ```typescript title="lib/auth-middleware.ts" theme={null} import { getServerSession } from 'next-auth' import { authOptions } from '@/lib/auth' import { NextRequest, NextResponse } from 'next/server' export async function withAuth(handler: Function) { return async (request: NextRequest, context: any) => { const session = await getServerSession(authOptions) if (!session) { return NextResponse.json( { error: '未授权访问' }, { status: 401 } ) } return handler(request, context) } } ``` ### 使用认证中间件 ```typescript title="app/api/protected/route.ts" theme={null} import { withAuth } from '@/lib/auth-middleware' export const GET = withAuth(async (request: NextRequest) => { // 需要认证的 API 逻辑 return NextResponse.json({ message: '受保护的内容' }) }) ``` ## 错误处理 ### 统一错误响应 ```typescript title="lib/api-response.ts" theme={null} import { NextResponse } from 'next/server' export function successResponse(data: any, status = 200) { return NextResponse.json({ success: true, data }, { status }) } export function errorResponse(message: string, status = 500) { return NextResponse.json({ success: false, error: message }, { status }) } ``` ### 错误处理示例 ```typescript title="app/api/users/route.ts" theme={null} import { successResponse, errorResponse } from '@/lib/api-response' export async function GET() { try { const users = await prisma.user.findMany() return successResponse(users) } catch (error) { console.error('获取用户失败:', error) return errorResponse('服务器错误') } } export async function POST(request: NextRequest) { try { const body = await request.json() // 验证数据 if (!body.email || !body.name) { return errorResponse('邮箱和姓名为必填项', 400) } const user = await prisma.user.create({ data: body }) return successResponse(user, 201) } catch (error) { if (error.code === 'P2002') { return errorResponse('邮箱已存在', 409) } return errorResponse('创建用户失败') } } ``` ## 常见场景 ### 聊天 API 示例 ```typescript title="app/api/chats/route.ts" theme={null} export async function GET(request: NextRequest) { const session = await getServerSession(authOptions) const chats = await prisma.chat.findMany({ where: { userId: session.user.id }, include: { messages: true }, orderBy: { updatedAt: 'desc' } }) return successResponse(chats) } export async function POST(request: NextRequest) { const session = await getServerSession(authOptions) const { title } = await request.json() const chat = await prisma.chat.create({ data: { title, userId: session.user.id } }) return successResponse(chat, 201) } ``` ### 文件上传 ```typescript title="app/api/upload/route.ts" theme={null} export async function POST(request: NextRequest) { const formData = await request.formData() const file = formData.get('file') as File if (!file) { return errorResponse('未找到文件', 400) } // 保存文件逻辑 const buffer = Buffer.from(await file.arrayBuffer()) const filename = `${Date.now()}-${file.name}` // 返回文件URL return successResponse({ url: `/uploads/${filename}`, filename }) } ``` ## 最佳实践 1. **使用 TypeScript** 确保类型安全 2. **统一响应格式** 便于前端处理 3. **添加错误处理** 提供有意义的错误信息 4. **验证输入数据** 防止无效数据 5. **使用中间件** 处理认证和权限 查看 [数据库功能](/features/database) 了解更多 Prisma 使用方法。 # 组件开发 Source: https://codofly.mintlify.app/development/components 学习如何在 Codofly Template 中创建和管理 React 组件 # 组件开发 本文档将指导您如何在 Codofly Template 中创建、管理和复用 React 组件。 ## 组件目录结构 ### 组件组织方式 ``` components/ ├── ui/ # 基础 UI 组件 (shadcn/ui) │ ├── button.tsx # 按钮组件 │ ├── input.tsx # 输入框组件 │ └── card.tsx # 卡片组件 ├── auth/ # 认证相关组件 │ ├── login-form.tsx # 登录表单 │ └── signup-form.tsx # 注册表单 ├── dashboard/ # 仪表盘组件 │ ├── sidebar.tsx # 侧边栏 │ └── stats-card.tsx # 统计卡片 ├── chat/ # 聊天功能组件 │ ├── chat-interface.tsx # 聊天界面 │ └── message-list.tsx # 消息列表 └── shared/ # 共享组件 ├── navbar.tsx # 导航栏 ├── footer.tsx # 页脚 └── loading.tsx # 加载指示器 ``` 组件按功能模块分组,基础 UI 组件放在 `ui/` 目录,业务组件按功能分组。 ## UI 组件 (shadcn/ui) ### 安装新组件 ```bash theme={null} npx shadcn-ui@latest add button npx shadcn-ui@latest add card npx shadcn-ui@latest add input ``` ### 基础使用示例 ```typescript Button.tsx theme={null} import { Button } from "@/components/ui/button" export function ExampleButton() { return ( 默认按钮 边框按钮 危险按钮 ) } ``` ```typescript Card.tsx theme={null} import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" export function ExampleCard() { return ( 卡片标题 卡片内容 ) } ``` ## 业务组件开发 ### 认证组件示例 ```typescript LoginForm.tsx theme={null} "use client" import { useState } from "react" import { Button } from "@/components/ui/button" import { Input } from "@/components/ui/input" import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" interface LoginFormProps { onSubmit?: (email: string, password: string) => void className?: string } export function LoginForm({ onSubmit, className }: LoginFormProps) { const [email, setEmail] = useState("") const [password, setPassword] = useState("") const handleSubmit = (e: React.FormEvent) => { e.preventDefault() onSubmit?.(email, password) } return ( 登录 setEmail(e.target.value)} /> setPassword(e.target.value)} /> 登录 ) } ``` ```typescript StatsCard.tsx theme={null} import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" import { LucideIcon } from "lucide-react" interface StatsCardProps { title: string value: string | number icon: LucideIcon trend?: { value: number isPositive: boolean } } export function StatsCard({ title, value, icon: Icon, trend }: StatsCardProps) { return ( {title} {value} {trend && ( {trend.isPositive ? '+' : ''}{trend.value}% 相比上月 )} ) } ``` ## TypeScript 类型定义 ### 基础组件类型 ```typescript title="types/components.ts" theme={null} import { ReactNode } from "react" import { LucideIcon } from "lucide-react" // 基础组件 Props export interface BaseComponentProps { className?: string children?: ReactNode } // 表单组件类型 export interface FormFieldProps { label: string name: string type?: "text" | "email" | "password" placeholder?: string required?: boolean value: string onChange: (value: string) => void } // 导航项类型 export interface NavigationItem { label: string href: string icon?: LucideIcon children?: NavigationItem[] } ``` ### 业务数据类型 ```typescript title="types/business.ts" theme={null} // 用户类型 export interface User { id: string email: string name: string avatar?: string role: "admin" | "user" } // 聊天消息类型 export interface ChatMessage { id: string content: string role: "user" | "assistant" timestamp: Date } // API 响应类型 export interface ApiResponse { success: boolean data?: T error?: string } ``` ## 样式处理 ### Tailwind CSS 使用 ```typescript title="components/styled-example.tsx" theme={null} import { cn } from "@/lib/utils" interface StyledCardProps { variant?: "default" | "outlined" className?: string children: React.ReactNode } export function StyledCard({ variant = "default", className, children }: StyledCardProps) { return ( {children} ) } ``` ### 响应式和深色模式 ```typescript title="components/responsive-example.tsx" theme={null} export function ResponsiveComponent() { return ( 支持深色模式的内容 ) } ``` ## 组件复用策略 ### 1. 组合组件 ```typescript title="components/form/index.tsx" theme={null} // 表单容器 export function Form({ children, onSubmit }: FormProps) { return {children} } // 表单字段 export function FormField({ label, children }: FormFieldProps) { return ( {label} {children} ) } // 使用示例 export function ContactForm() { return ( 提交 ) } ``` ### 2. 自定义 Hook ```typescript title="hooks/use-form.ts" theme={null} import { useState } from "react" export function useForm(initialValues: T) { const [values, setValues] = useState(initialValues) const [errors, setErrors] = useState>({}) const setValue = (name: keyof T, value: any) => { setValues(prev => ({ ...prev, [name]: value })) } const reset = () => { setValues(initialValues) setErrors({}) } return { values, errors, setValue, reset } } ``` ### 3. 高阶组件 ```typescript title="components/hoc/with-loading.tsx" theme={null} import { ComponentType } from "react" export function withLoading( Component: ComponentType ) { return function LoadingComponent(props: T & { loading?: boolean }) { const { loading, ...rest } = props if (loading) { return 加载中... } return } } ``` ## 最佳实践 ### 1. 组件命名 * 使用 PascalCase 命名组件 * 文件名与组件名保持一致 * 使用描述性的名称 ### 2. Props 设计 ```typescript theme={null} // ✅ 好的做法 interface ButtonProps { variant?: "primary" | "secondary" size?: "sm" | "md" | "lg" disabled?: boolean children: React.ReactNode } // ❌ 避免的做法 interface ButtonProps { type?: string // 太模糊 data?: any // 类型不明确 } ``` ### 3. 性能优化 ```typescript theme={null} import { memo, useMemo } from "react" // 使用 memo 避免不必要的重渲染 export const ExpensiveComponent = memo(({ data }: Props) => { const processedData = useMemo(() => { return data.map(item => /* 复杂计算 */) }, [data]) return {/* 渲染内容 */} }) ``` 合理使用 memo 和 useMemo,避免过度优化导致代码复杂。 ## 总结 在 Codofly Template 中开发组件时: * 使用 shadcn/ui 作为基础 UI 组件 * 按功能模块组织组件目录 * 使用 TypeScript 确保类型安全 * 利用 Tailwind CSS 进行样式设计 * 通过组合和复用提高开发效率 掌握这些概念后,您就能高效地创建和管理组件了。 # 部署指南 Source: https://codofly.mintlify.app/development/deployment 将 Codofly Template 部署到生产环境 # 部署指南 本文档将指导您如何将 Codofly Template 部署到 Vercel 生产环境。 ## Vercel 部署 ### 连接 GitHub 仓库 访问 [vercel.com](https://vercel.com) 并使用 GitHub 账号登录 点击 "New Project" → "Import Git Repository" → 选择你的项目仓库 * Framework Preset: 选择 "Next.js" * Build Command: `pnpm build` * Install Command: `pnpm install` 点击 "Deploy" 开始部署 ### 环境变量配置 在 Vercel 项目设置中添加以下环境变量: ```bash theme={null} # 数据库 DATABASE_URL=your_production_database_url # NextAuth NEXTAUTH_URL=https://your-domain.vercel.app NEXTAUTH_SECRET=your_production_secret # OAuth 提供商 GITHUB_CLIENT_ID=your_github_client_id GITHUB_CLIENT_SECRET=your_github_client_secret GOOGLE_CLIENT_ID=your_google_client_id GOOGLE_CLIENT_SECRET=your_google_client_secret # Stripe STRIPE_SECRET_KEY=your_stripe_secret_key STRIPE_WEBHOOK_SECRET=your_stripe_webhook_secret # AI 服务 OPENAI_API_KEY=your_openai_api_key ANTHROPIC_API_KEY=your_anthropic_api_key ``` 确保在生产环境中使用强密码和真实的 API 密钥。 ### 自定义域名 在 Vercel 项目设置 → Domains 中添加你的域名 在域名提供商处添加 CNAME 记录指向 Vercel 等待 DNS 生效并验证域名 ## 数据库部署 ### Vercel Postgres ```bash theme={null} # 安装 Vercel Postgres pnpm add @vercel/postgres # 在 Vercel 控制台创建数据库 # 获取连接字符串并添加到环境变量 DATABASE_URL=postgres://user:pass@host:port/db?sslmode=require ``` ### Neon 数据库 ```bash theme={null} # 1. 在 neon.tech 创建数据库 # 2. 获取连接字符串 DATABASE_URL=postgresql://user:pass@host/db?sslmode=require # 3. 运行迁移 npx prisma migrate deploy ``` ### 生产环境迁移 ```bash theme={null} # 部署数据库迁移 npx prisma migrate deploy # 生成 Prisma Client npx prisma generate # 添加种子数据(可选) npx prisma db seed ``` ## 必需环境变量 ### 核心变量 ```bash theme={null} DATABASE_URL= # 数据库连接 NEXTAUTH_URL= # 应用域名 NEXTAUTH_SECRET= # JWT 签名密钥(随机字符串) ``` ### 第三方服务 ```bash theme={null} # OAuth GITHUB_CLIENT_ID= GITHUB_CLIENT_SECRET= GOOGLE_CLIENT_ID= GOOGLE_CLIENT_SECRET= # 支付 STRIPE_SECRET_KEY= STRIPE_WEBHOOK_SECRET= # AI 服务 OPENAI_API_KEY= ANTHROPIC_API_KEY= ``` ### 安全配置建议 * 使用强随机密钥作为 `NEXTAUTH_SECRET` * 在生产环境中禁用调试模式 * 定期轮换 API 密钥 * 限制 OAuth 回调 URL ## 性能优化 ### 构建优化 ```javascript title="next.config.js" theme={null} /** @type {import('next').NextConfig} */ const nextConfig = { experimental: { ppr: true, }, compress: true, poweredByHeader: false, images: { domains: ['your-image-domains.com'], }, } module.exports = nextConfig ``` ### 缓存策略 ```typescript title="app/api/users/route.ts" theme={null} export async function GET() { const users = await prisma.user.findMany() return NextResponse.json( { data: users }, { headers: { 'Cache-Control': 'public, s-maxage=60, stale-while-revalidate=300' } } ) } ``` ## 常见问题 ### 部署失败 ```bash theme={null} # 检查构建日志 # 确保所有依赖已安装 # 检查环境变量配置 ``` ### 数据库连接失败 ```bash theme={null} # 检查 DATABASE_URL 格式 # 确保数据库允许外部连接 # 运行数据库迁移 ``` ### 域名无法访问 ```bash theme={null} # 检查 DNS 配置 # 确保 NEXTAUTH_URL 正确 # 清除浏览器缓存 ``` 部署完成后,建议进行完整的功能测试,确保所有功能正常工作。 # 页面开发 Source: https://codofly.mintlify.app/development/pages 学习如何在 Codofly Template 中使用 Next.js App Router 创建和管理页面 # 页面开发 本文档将指导您如何在 Codofly Template 中使用 Next.js 15 的 App Router 创建和管理页面。 ## Next.js App Router 基础 ### App 目录结构 Next.js 15 使用 `app` 目录来定义路由结构,每个文件夹代表一个路由段: ``` app/ ├── page.tsx # 首页 (/) ├── layout.tsx # 根布局 ├── loading.tsx # 加载页面 ├── error.tsx # 错误页面 ├── not-found.tsx # 404 页面 ├── dashboard/ # /dashboard │ ├── page.tsx # 仪表盘页面 │ ├── layout.tsx # 仪表盘布局 │ └── settings/ # /dashboard/settings │ └── page.tsx # 设置页面 ├── [locale]/ # 国际化路由 │ ├── page.tsx # 本地化首页 │ └── about/ # /[locale]/about │ └── page.tsx # 关于页面 └── api/ # API 路由 └── users/ └── route.ts # API 端点 ``` ### 路由规则 在 App Router 中,只有 `page.tsx` 文件会创建可访问的路由。其他特殊文件如 `layout.tsx`、`loading.tsx` 等有特定用途。 **特殊文件说明:** * `page.tsx` - 页面组件,定义路由的 UI * `layout.tsx` - 布局组件,包装子页面 * `loading.tsx` - 加载状态 UI * `error.tsx` - 错误边界 UI * `not-found.tsx` - 404 页面 UI * `route.ts` - API 路由处理器 ## 创建新页面 ### 基础页面创建 1. 在 `app` 目录下创建新文件夹(路由名称) 2. 在文件夹内创建 `page.tsx` 文件 **示例:创建产品页面** ```typescript title="app/products/page.tsx" theme={null} import { Metadata } from 'next'; export const metadata: Metadata = { title: '产品 - Codofly', description: '探索 Codofly 的强大功能', }; export default function ProductsPage() { return ( 产品列表 {/* 产品列表内容 */} ); } ``` ### 带参数的页面 使用方括号 `[]` 创建动态路由: ```typescript title="app/products/[id]/page.tsx" theme={null} import { Metadata } from 'next'; import { notFound } from 'next/navigation'; interface Props { params: { id: string }; searchParams: { [key: string]: string | string[] | undefined }; } export async function generateMetadata({ params }: Props): Promise { const product = await getProduct(params.id); if (!product) { return { title: '产品未找到', }; } return { title: `${product.name} - Codofly`, description: product.description, }; } async function getProduct(id: string) { // 从数据库或 API 获取产品信息 // 这里是示例代码 try { const response = await fetch(`/api/products/${id}`); if (!response.ok) return null; return await response.json(); } catch { return null; } } export default async function ProductPage({ params }: Props) { const product = await getProduct(params.id); if (!product) { notFound(); } return ( {product.name} {product.description} ); } ``` ## 页面布局 ### 根布局 根布局是必需的,包装所有页面: ```typescript title="app/layout.tsx" theme={null} import { Inter } from 'next/font/google'; import { Providers } from '@/components/providers'; import { Navbar } from '@/components/navbar'; import { Footer } from '@/components/footer'; import './globals.css'; const inter = Inter({ subsets: ['latin'] }); export const metadata = { title: { default: 'Codofly Template', template: '%s | Codofly', }, description: 'AI SaaS 应用开发模板', }; export default function RootLayout({ children, }: { children: React.ReactNode; }) { return ( {children} ); } ``` ### 嵌套布局 为特定路由段创建专用布局: ```typescript title="app/dashboard/layout.tsx" theme={null} import { Sidebar } from '@/components/dashboard/sidebar'; import { DashboardProvider } from '@/contexts/dashboard-context'; export default function DashboardLayout({ children, }: { children: React.ReactNode; }) { return ( {children} ); } ``` ## 动态路由 ### 单个参数路由 ```typescript title="app/users/[id]/page.tsx" theme={null} interface Props { params: { id: string }; } export default function UserPage({ params }: Props) { return 用户 ID: {params.id}; } ``` ### 多个参数路由 ```typescript title="app/users/[id]/posts/[postId]/page.tsx" theme={null} interface Props { params: { id: string; postId: string; }; } export default function UserPostPage({ params }: Props) { return ( 用户 ID: {params.id} 文章 ID: {params.postId} ); } ``` ### 捕获所有路由 使用 `[...slug]` 捕获多个路由段: ```typescript title="app/docs/[...slug]/page.tsx" theme={null} interface Props { params: { slug: string[] }; } export default function DocsPage({ params }: Props) { const path = params.slug.join('/'); return 文档路径: {path}; } ``` `[...slug]` 会匹配 `/docs/a`、`/docs/a/b`、`/docs/a/b/c` 等所有路径。 ## 页面元数据 ### 静态元数据 ```typescript title="app/about/page.tsx" theme={null} import { Metadata } from 'next'; export const metadata: Metadata = { title: '关于我们', description: 'Codofly 团队介绍', keywords: ['AI', 'SaaS', '团队'], openGraph: { title: '关于我们 - Codofly', description: 'Codofly 团队介绍', images: ['/images/about-og.jpg'], }, twitter: { card: 'summary_large_image', title: '关于我们 - Codofly', description: 'Codofly 团队介绍', images: ['/images/about-twitter.jpg'], }, }; export default function AboutPage() { return 关于我们页面内容; } ``` ### 动态元数据 ```typescript title="app/blog/[slug]/page.tsx" theme={null} import { Metadata } from 'next'; interface Props { params: { slug: string }; } export async function generateMetadata({ params }: Props): Promise { const post = await getPost(params.slug); if (!post) { return { title: '文章未找到', }; } return { title: post.title, description: post.excerpt, openGraph: { title: post.title, description: post.excerpt, images: [post.coverImage], type: 'article', publishedTime: post.publishedAt, authors: [post.author.name], }, }; } async function getPost(slug: string) { // 获取文章数据的逻辑 } export default async function BlogPostPage({ params }: Props) { const post = await getPost(params.slug); return {/* 文章内容 */}; } ``` ## 国际化页面 ### 设置国际化路由 在 Codofly Template 中,国际化通过 `[locale]` 路由实现: ```typescript title="app/[locale]/page.tsx" theme={null} import { setRequestLocale } from 'next-intl/server'; import { useTranslations } from 'next-intl'; interface Props { params: { locale: string }; } export default function HomePage({ params: { locale } }: Props) { // 启用静态渲染 setRequestLocale(locale); const t = useTranslations('HomePage'); return ( {t('title')} {t('description')} ); } ``` ### 国际化布局 ```typescript title="app/[locale]/layout.tsx" theme={null} import { NextIntlClientProvider } from 'next-intl'; import { getMessages } from 'next-intl/server'; import { setRequestLocale } from 'next-intl/server'; interface Props { children: React.ReactNode; params: { locale: string }; } export default async function LocaleLayout({ children, params: { locale } }: Props) { // 启用静态渲染 setRequestLocale(locale); const messages = await getMessages(); return ( {children} ); } ``` ### 多语言元数据 ```typescript title="app/[locale]/about/page.tsx" theme={null} import { Metadata } from 'next'; import { getTranslations } from 'next-intl/server'; interface Props { params: { locale: string }; } export async function generateMetadata({ params: { locale } }: Props): Promise { const t = await getTranslations({ locale, namespace: 'AboutPage' }); return { title: t('meta.title'), description: t('meta.description'), }; } export default function AboutPage({ params: { locale } }: Props) { return 本地化的关于页面; } ``` ## 最佳实践 ### 1. 页面组件结构 ```typescript theme={null} // 推荐的页面组件结构 export default function ProductPage() { return ( {/* 页面头部 */} 页面标题 {/* 主要内容 */} {/* 内容区域 */} {/* 页面底部(如需要) */} ); } ``` ### 2. 错误处理 ```typescript title="app/products/error.tsx" theme={null} 'use client'; import { useEffect } from 'react'; export default function Error({ error, reset, }: { error: Error & { digest?: string }; reset: () => void; }) { useEffect(() => { console.error(error); }, [error]); return ( 出错了! 重试 ); } ``` ### 3. 加载状态 ```typescript title="app/products/loading.tsx" theme={null} export default function Loading() { return ( {Array.from({ length: 6 }).map((_, i) => ( ))} ); } ``` 确保在使用客户端组件时添加 `'use client'` 指令,在使用服务器端功能时避免使用此指令。 ## 总结 通过 Next.js App Router,您可以: * 使用文件系统路由快速创建页面 * 通过嵌套布局实现复杂的页面结构 * 利用动态路由处理参数化页面 * 通过元数据 API 优化 SEO * 实现完整的国际化支持 掌握这些概念后,您就能在 Codofly Template 中高效地创建和管理页面了。 # 常见问题 Source: https://codofly.mintlify.app/faq Codofly Template 常见问题及解答 # 常见问题 这里列出了一些常见问题及其解答。 ## 安装问题 在安装依赖时可能会遇到各种问题,以下是常见解决方案: 1. **清除缓存后重新安装** ```bash theme={null} # 使用 npm npm cache clean --force npm install # 使用 yarn yarn cache clean yarn install # 使用 pnpm pnpm store prune pnpm install ``` 2. **检查 Node.js 和包管理器版本** 确保您使用的 Node.js 版本与项目兼容(推荐 Node.js 18+)。 ```bash theme={null} node -v npm -v # 或 yarn -v 或 pnpm -v ``` 3. **使用特定的 registry** 如果您在中国大陆,可以尝试使用淘宝镜像: ```bash theme={null} # 临时使用 npm install --registry=https://registry.npmmirror.com # 或永久设置 npm config set registry https://registry.npmmirror.com ``` 4. **检查网络问题** 某些依赖可能因为网络问题下载失败,特别是涉及到 GitHub 的依赖。尝试使用代理或 VPN。 5. **安装失败的具体依赖** 如果只有特定依赖安装失败,可以尝试单独安装: ```bash theme={null} npm install problematic-package --force ``` Codofly Template 基于 Next.js 15 构建,建议使用以下 Node.js 版本: * **推荐版本**: Node.js 18.x 或 20.x * **最低版本**: Node.js 18.17.0 * **不支持版本**: Node.js 16.x 及以下版本 如果您需要管理多个 Node.js 版本,建议使用 nvm(Node Version Manager): ```bash theme={null} # 安装特定 Node.js 版本 nvm install 18.17.0 # 切换 Node.js 版本 nvm use 18.17.0 ``` 如果出现与 Node.js 版本相关的错误,检查 package.json 中的 engines 字段: ```json theme={null} "engines": { "node": ">=18.17.0" } ``` 使用不兼容的 Node.js 版本可能导致以下问题: * 构建错误 * 运行时错误 * 依赖冲突 * 性能问题 Codofly Template 使用 Prisma ORM 连接 PostgreSQL 数据库,常见连接问题及解决方案: 1. **连接字符串格式错误** 确保您的 DATABASE\_URL 环境变量格式正确: ``` DATABASE_URL="postgresql://username:password@hostname:port/database" ``` 2. **数据库服务器未运行** 确认数据库服务器正在运行,并且可以从应用服务器访问: ```bash theme={null} # 检查数据库连接 npx prisma db ping ``` 3. **防火墙或网络问题** 如果数据库在远程服务器上,确保端口已开放(PostgreSQL 默认为 5432)。 4. **Prisma 模型与数据库不同步** 运行 Prisma 迁移同步数据库结构: ```bash theme={null} npx prisma migrate dev # 或在生产环境 npx prisma migrate deploy ``` 5. **SSL 要求** 如果您的数据库要求 SSL 连接,请在连接字符串中添加相应参数: ``` DATABASE_URL="postgresql://username:password@hostname:port/database?sslmode=require" ``` 6. **使用 Prisma 调试** 开启 Prisma 调试日志查看详细连接信息: ```bash theme={null} DEBUG="prisma:*" npm run dev ``` ## 配置问题 环境变量配置问题是最常见的启动错误来源。以下是解决方案: 1. **确保创建了正确的环境文件** 开发环境应创建 `.env.local` 文件,检查是否从示例文件正确复制: ```bash theme={null} cp .env.example .env.local ``` 2. **检查必需的环境变量** 确保所有必需的环境变量都已配置,特别是: ``` # 数据库 DATABASE_URL= # 认证 NEXTAUTH_SECRET= NEXTAUTH_URL= # AI 提供商密钥 OPENAI_API_KEY= # 支付相关 STRIPE_SECRET_KEY= NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY= STRIPE_WEBHOOK_SECRET= ``` 3. **检查环境变量格式** 确保没有多余的空格、引号或特殊字符: ``` # 错误 DATABASE_URL = "postgresql://..." # 等号周围有空格 # 正确 DATABASE_URL="postgresql://..." ``` 4. **环境变量加载问题** 如果环境变量似乎没有被加载,尝试使用 `dotenv` 显式加载: ```javascript theme={null} // 在服务器启动脚本中 require('dotenv').config({ path: '.env.local' }); ``` 5. **检查不同环境的配置** 开发环境使用 `.env.local`,生产环境通常在部署平台(如 Vercel)配置。 Stripe 支付集成是 Codofly Template 的关键功能,以下是常见配置问题及解决方案: 1. **API 密钥不正确** 确保使用了正确的 Stripe API 密钥,区分测试模式和生产模式密钥: ``` # 测试模式 STRIPE_SECRET_KEY=sk_test_... NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_... # 生产模式 STRIPE_SECRET_KEY=sk_live_... NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_live_... ``` 2. **Webhook 设置问题** Stripe Webhooks 需要正确配置才能处理支付事件: * 在 Stripe 仪表板创建 webhook * 添加端点 URL(例如 `https://yourdomain.com/api/stripe/webhooks`) * 选择需要监听的事件(至少 `checkout.session.completed`) * 获取 Webhook 密钥并配置环境变量: ``` STRIPE_WEBHOOK_SECRET=whsec_... ``` 3. **本地开发中测试 Webhooks** 使用 Stripe CLI 在本地测试 Webhooks: ```bash theme={null} # 安装 Stripe CLI brew install stripe/stripe-cli/stripe # 登录 stripe login # 转发 webhooks 到本地 stripe listen --forward-to localhost:3000/api/stripe/webhooks ``` 4. **产品和价格配置** 确保在 Stripe 仪表板中创建了产品和价格,并在代码中正确引用价格 ID: ```javascript theme={null} const checkoutSession = await stripe.checkout.sessions.create({ line_items: [ { price: 'price_1234567890', // 从 Stripe 仪表板获取的价格 ID quantity: 1, }, ], // ...其他配置 }); ``` 5. **货币不匹配问题** 确保应用中使用的货币与 Stripe 价格设置匹配。 NextAuth.js(Auth.js)是 Codofly Template 的认证系统,以下是常见配置问题: 1. **基本配置缺失** 确保设置了基本的 NextAuth 环境变量: ``` NEXTAUTH_SECRET=your_random_secure_string NEXTAUTH_URL=http://localhost:3000 # 开发环境 NEXTAUTH_URL=https://yourdomain.com # 生产环境 ``` 2. **OAuth 提供商配置** 配置社交登录提供商的 Client ID 和 Secret: ``` # GitHub GITHUB_ID=your_github_client_id GITHUB_SECRET=your_github_client_secret # Google GOOGLE_ID=your_google_client_id GOOGLE_SECRET=your_google_client_secret ``` 3. **数据库适配器问题** 确保 Prisma 适配器配置正确: ```javascript theme={null} import { PrismaAdapter } from "@auth/prisma-adapter"; import { prisma } from "@/lib/prisma"; export const { handlers, auth, signIn, signOut } = NextAuth({ adapter: PrismaAdapter(prisma), // ...其他配置 }); ``` 4. **回调 URL 设置** 在 OAuth 提供商的开发者控制台中,确保添加了正确的回调 URL: ``` http://localhost:3000/api/auth/callback/github http://localhost:3000/api/auth/callback/google https://yourdomain.com/api/auth/callback/github https://yourdomain.com/api/auth/callback/google ``` 5. **会话配置问题** 根据需要配置会话策略(JWT 或数据库): ```javascript theme={null} export const { handlers, auth, signIn, signOut } = NextAuth({ session: { strategy: "jwt", // 或 "database" }, // ...其他配置 }); ``` 6. **自定义页面路由** 如果使用自定义登录页面,确保正确配置: ```javascript theme={null} export const { handlers, auth, signIn, signOut } = NextAuth({ pages: { signIn: "/auth/login", signOut: "/auth/logout", error: "/auth/error", }, // ...其他配置 }); ``` ## 开发问题 在 Codofly Template 中添加新页面遵循 Next.js App Router 的规范: 1. **创建新的路由目录** 在 `app/[locale]` 目录下创建相应的目录结构: ``` app/ [locale]/ (workspace)/ # 分组路由 new-feature/ # 新功能路径 page.tsx # 页面组件 ``` 2. **创建页面组件** 创建 `page.tsx` 文件,这是路由的主要入口点: ```tsx theme={null} import { Metadata } from "next"; export const metadata: Metadata = { title: "新功能页面", description: "这是一个新功能页面的描述", }; export default function NewFeaturePage() { return ( 新功能页面 这是新功能页面的内容 ); } ``` 3. **添加国际化支持** 在 `messages` 目录下的语言文件中添加翻译: ```ts theme={null} // messages/zh.json { "NewFeature": { "title": "新功能页面", "description": "这是新功能页面的描述" } } // messages/en.json { "NewFeature": { "title": "New Feature Page", "description": "This is a description of the new feature page" } } ``` 然后在页面中使用翻译: ```tsx theme={null} import { useTranslations } from "next-intl"; export default function NewFeaturePage() { const t = useTranslations("NewFeature"); return ( {t("title")} {t("description")} ); } ``` 4. **添加页面到导航** 在导航组件中添加新页面链接: ```tsx theme={null} import { Link } from "@/navigation"; // 在导航组件中 {t("navigation.newFeature")} ``` 5. **添加访问控制(如需要)** 如果页面需要认证,添加适当的权限检查: ```tsx theme={null} import { redirect } from "next/navigation"; import { auth } from "@/auth"; export default async function ProtectedPage() { const session = await auth(); if (!session) { redirect("/auth/login"); } return ( // 页面内容 ); } ``` Codofly Template 使用 Tailwind CSS 进行样式设计,您可以通过以下方式自定义主题: 1. **修改 Tailwind 配置** 编辑 `tailwind.config.ts` 文件自定义颜色、字体等: ```typescript theme={null} import type { Config } from "tailwindcss"; const config: Config = { theme: { extend: { colors: { primary: { DEFAULT: "#3B82F6", 50: "#EFF6FF", // ...其他色调 900: "#1E3A8A", }, // 添加自定义颜色 secondary: { DEFAULT: "#10B981", // ...色调 }, // 自定义品牌颜色 brand: "#FF6347", }, fontFamily: { sans: ["var(--font-sans)", "system-ui", "sans-serif"], // 添加自定义字体 display: ["var(--font-display)", "sans-serif"], }, // 自定义其他设计变量 borderRadius: { custom: "0.5rem", }, }, }, // ...其他配置 }; export default config; ``` 2. **自定义全局样式** 编辑 `app/globals.css` 文件添加自定义 CSS 变量和样式: ```css theme={null} @tailwind base; @tailwind components; @tailwind utilities; @layer base { :root { --background: 0 0% 100%; --foreground: 222.2 84% 4.9%; --primary: 221.2 83.2% 53.3%; --primary-foreground: 210 40% 98%; /* 添加自定义 CSS 变量 */ --brand-color: 9 100% 64%; --brand-foreground: 0 0% 100%; } .dark { --background: 222.2 84% 4.9%; --foreground: 210 40% 98%; /* 暗色模式变量 */ --brand-color: 9 100% 54%; } } /* 添加自定义工具类 */ @layer utilities { .custom-shadow { box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08); } } /* 添加自定义组件类 */ @layer components { .card-hover { @apply transition-all duration-200 hover:scale-105 hover:shadow-lg; } } ``` 3. **修改组件主题** Codofly Template 使用 shadcn/ui 组件,您可以通过修改 `components/ui/theme.js` 文件自定义组件样式: ```javascript theme={null} const theme = { button: { base: "inline-flex items-center justify-center rounded-md font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring", // 自定义变体 variant: { brand: "bg-[hsl(var(--brand-color))] text-white hover:bg-[hsl(var(--brand-color))/90]", }, // 自定义尺寸 size: { xl: "h-14 px-8 text-lg", }, }, // 其他组件自定义 }; ``` 4. **深色模式切换** Codofly Template 已内置深色模式支持,可以使用主题切换组件: ```tsx theme={null} import { ThemeToggle } from "@/components/theme-toggle"; // 在页面或布局中 ``` 5. **自定义品牌资源** 替换以下文件以自定义品牌资源: * `public/logo.svg` - 主要徽标 * `public/favicon.ico` - 网站图标 * `app/[locale]/opengraph-image.png` - 社交媒体预览图 Codofly Template 支持多种 AI 模型提供商,添加新的 AI 模型需要几个步骤: 1. **更新模型配置** 编辑 `lib/models.ts` 文件,添加新的模型定义: ```typescript theme={null} export const models: LLMModel[] = [ // 现有模型... // 添加新模型 { id: "new-model-name", name: "新模型名称", description: "新模型的简短描述", maxTokens: 16000, // 模型最大标记数 inputPrice: 0.01, // 每千输入标记价格(美元) outputPrice: 0.03, // 每千输出标记价格(美元) provider: { // 提供商信息 id: "provider-name", // 例如 "openai", "anthropic" 等 name: "提供商名称", }, available: true, // 是否可用 }, ]; ``` 2. **实现模型客户端创建函数** 在 `getModelClient` 函数中添加新的提供商支持: ```typescript theme={null} export function getModelClient(model: LLMModel, config: LLMModelConfig) { const { id: modelNameString, providerId } = model; const { apiKey = process.env.OPENAI_API_KEY, baseURL = process.env.OPENAI_API_BASE_URL, } = config; const providerConfigs = { openai: () => createOpenAI({ apiKey, baseURL })(modelNameString), anthropic: () => createAnthropic({ apiKey, baseURL })(modelNameString), // 添加新的提供商 "new-provider": () => createNewProvider({ apiKey, baseURL })(modelNameString), }; const createClient = providerConfigs[providerId as keyof typeof providerConfigs]; if (!createClient) { return providerConfigs.openai(); } return createClient(); } // 为新提供商实现创建客户端的函数 function createNewProvider({ apiKey, baseURL }: ProviderConfig) { return (modelName: string) => { // 实现与新提供商 API 的集成 // 返回兼容的客户端对象 return { // 实现兼容 AI SDK 的接口 }; }; } ``` 3. **添加环境变量** 在 `.env.local` 和生产环境中添加新的 API 密钥: ``` NEW_PROVIDER_API_KEY=your_api_key_here NEW_PROVIDER_API_BASE_URL=https://api.provider.com ``` 4. **更新 UI 选择器** 确保模型选择器组件显示新的模型: ```tsx theme={null} // components/model-selector.tsx import { models } from "@/lib/models"; export function ModelSelector({ value, onChange }) { return ( {models .filter(model => model.available) .map(model => ( {model.name} {model.provider.name} ))} ); } ``` 5. **实现计费逻辑** 确保计费系统支持新的模型: ```typescript theme={null} // lib/billing.ts export function calculateCost( inputTokens: number, outputTokens: number, model: LLMModel, ): number { // 使用模型定义中的价格 const inputPrice = model.inputPrice ?? 0; const outputPrice = model.outputPrice ?? 0; // 计算成本 const inputCost = (inputTokens * inputPrice) / 1000; const outputCost = (outputTokens * outputPrice) / 1000; return inputCost + outputCost; } ``` 6. **测试新模型** 在开发环境中测试新模型的请求、响应和计费逻辑,确保一切正常工作。 # AI 集成 Source: https://codofly.mintlify.app/features/ai-integration 集成多种 AI 模型提供智能对话功能 # AI 集成 Codofly Template 支持多种 AI 模型,包括 OpenAI、Anthropic 和 Google,提供灵活的 AI 对话功能。 ## AI 模型配置 ### 环境变量 ```bash theme={null} # OpenAI OPENAI_API_KEY=sk-... OPENAI_API_BASE_URL=https://api.openai.com/v1 # Anthropic ANTHROPIC_API_KEY=sk-ant-... # Google GOOGLE_API_KEY=AIza... ``` ### 模型配置 ```typescript title="lib/ai/models.ts" theme={null} export interface LLMModel { id: string name: string providerId: 'openai' | 'anthropic' | 'google' maxTokens: number costPerToken: number } export const AVAILABLE_MODELS: LLMModel[] = [ { id: 'gpt-4o', name: 'GPT-4o', providerId: 'openai', maxTokens: 128000, costPerToken: 0.00001 }, { id: 'claude-3-5-sonnet-20241022', name: 'Claude 3.5 Sonnet', providerId: 'anthropic', maxTokens: 200000, costPerToken: 0.000015 }, { id: 'gemini-2.0-flash-exp', name: 'Gemini 2.0 Flash', providerId: 'google', maxTokens: 100000, costPerToken: 0.000008 } ] ``` ## 文本生成 ### AI 客户端配置 ```typescript title="lib/ai/client.ts" theme={null} import { openai } from '@ai-sdk/openai' import { anthropic } from '@ai-sdk/anthropic' import { google } from '@ai-sdk/google' import { generateText, streamText } from 'ai' export function getModelClient(model: LLMModel) { const config = { apiKey: getApiKey(model.providerId), baseURL: getBaseURL(model.providerId) } switch (model.providerId) { case 'openai': return openai(model.id, config) case 'anthropic': return anthropic(model.id, config) case 'google': return google(model.id, config) default: throw new Error(`不支持的模型提供商: ${model.providerId}`) } } function getApiKey(providerId: string): string { switch (providerId) { case 'openai': return process.env.OPENAI_API_KEY! case 'anthropic': return process.env.ANTHROPIC_API_KEY! case 'google': return process.env.GOOGLE_API_KEY! default: throw new Error(`未找到 ${providerId} 的 API 密钥`) } } ``` ### 基础文本生成 ```typescript title="app/api/ai/generate/route.ts" theme={null} import { generateText } from 'ai' import { getModelClient } from '@/lib/ai/client' import { auth } from '@/lib/auth' import { consumeCredits } from '@/lib/credits' export async function POST(request: Request) { const session = await auth() if (!session) { return NextResponse.json({ error: '未授权' }, { status: 401 }) } const { messages, modelId } = await request.json() try { const model = AVAILABLE_MODELS.find(m => m.id === modelId) if (!model) { return NextResponse.json({ error: '不支持的模型' }, { status: 400 }) } const client = getModelClient(model) const result = await generateText({ model: client, messages, maxTokens: 1000, }) // 计算和扣除积分 const cost = calculateCost(result.usage, model) await consumeCredits(session.user.id, cost) return NextResponse.json({ text: result.text, usage: result.usage, cost }) } catch (error) { console.error('AI 生成失败:', error) return NextResponse.json({ error: 'AI 服务暂时不可用' }, { status: 500 }) } } function calculateCost(usage: any, model: LLMModel): number { const totalTokens = usage.promptTokens + usage.completionTokens return Math.ceil(totalTokens * model.costPerToken * 1000) // 转换为积分 } ``` ## 流式响应 ### 流式文本生成 ```typescript title="app/api/ai/stream/route.ts" theme={null} import { streamText } from 'ai' import { getModelClient } from '@/lib/ai/client' export async function POST(request: Request) { const session = await auth() if (!session) { return NextResponse.json({ error: '未授权' }, { status: 401 }) } const { messages, modelId } = await request.json() const model = AVAILABLE_MODELS.find(m => m.id === modelId) if (!model) { return NextResponse.json({ error: '不支持的模型' }, { status: 400 }) } const client = getModelClient(model) const result = await streamText({ model: client, messages, maxTokens: 1000, onFinish: async (result) => { // 流式完成后扣除积分 const cost = calculateCost(result.usage, model) await consumeCredits(session.user.id, cost) // 保存对话记录 await saveConversation(session.user.id, messages, result.text) } }) return result.toAIStreamResponse() } ``` ### 客户端流式组件 ```typescript title="components/ai/streaming-chat.tsx" theme={null} 'use client' import { useState } from 'react' import { useChat } from 'ai/react' import { Button } from '@/components/ui/button' import { Input } from '@/components/ui/input' export function StreamingChat() { const [selectedModel, setSelectedModel] = useState('gpt-4o') const { messages, input, handleInputChange, handleSubmit, isLoading } = useChat({ api: '/api/ai/stream', body: { modelId: selectedModel } }) return ( {/* 模型选择 */} setSelectedModel(e.target.value)} className="border rounded px-3 py-1" > {AVAILABLE_MODELS.map(model => ( {model.name} ))} {/* 消息列表 */} {messages.map(message => ( {message.content} ))} {isLoading && ( )} {/* 输入框 */} 发送 ) } ``` ## 成本控制 ### 积分计算 ```typescript title="lib/ai/pricing.ts" theme={null} export function calculateTokenCost( usage: { promptTokens: number; completionTokens: number }, model: LLMModel ): number { const totalTokens = usage.promptTokens + usage.completionTokens return Math.ceil(totalTokens * model.costPerToken * 1000) } export function estimateMessageCost(message: string, model: LLMModel): number { // 简单估算:4 个字符 ≈ 1 个 token const estimatedTokens = Math.ceil(message.length / 4) return Math.ceil(estimatedTokens * model.costPerToken * 1000) } ``` ### 用量限制 ```typescript title="lib/ai/limits.ts" theme={null} export async function checkUsageLimit(userId: string, estimatedCost: number) { const user = await prisma.user.findUnique({ where: { id: userId }, select: { credits: true, plan: true } }) if (!user) { throw new Error('用户不存在') } if (user.credits < estimatedCost) { throw new Error('积分不足,请充值后再试') } // 检查每日使用限制 const today = new Date() today.setHours(0, 0, 0, 0) const todayUsage = await prisma.creditTransaction.aggregate({ where: { userId, type: 'CONSUMPTION', createdAt: { gte: today } }, _sum: { amount: true } }) const dailyLimit = getDailyLimit(user.plan) const usedToday = Math.abs(todayUsage._sum.amount || 0) if (usedToday + estimatedCost > dailyLimit) { throw new Error('今日用量已达上限') } return true } function getDailyLimit(plan: string): number { switch (plan) { case 'FREE': return 100 case 'BASIC': return 1000 case 'PRO': return 10000 default: return 100 } } ``` ## 多模型支持 ### 模型切换 ```typescript title="components/ai/model-selector.tsx" theme={null} 'use client' import { useState } from 'react' import { AVAILABLE_MODELS } from '@/lib/ai/models' interface ModelSelectorProps { selectedModel: string onModelChange: (modelId: string) => void } export function ModelSelector({ selectedModel, onModelChange }: ModelSelectorProps) { return ( 选择 AI 模型 {AVAILABLE_MODELS.map(model => ( onModelChange(model.id)} > {model.name} {model.providerId} • {model.maxTokens.toLocaleString()} tokens {model.costPerToken * 1000}积分/1K tokens ))} ) } ``` ## 错误处理 ### AI 服务错误处理 ```typescript title="lib/ai/error-handler.ts" theme={null} export class AIError extends Error { constructor( message: string, public code: string, public status: number = 500 ) { super(message) this.name = 'AIError' } } export function handleAIError(error: any): AIError { if (error.status === 401) { return new AIError('API 密钥无效', 'INVALID_API_KEY', 401) } if (error.status === 429) { return new AIError('请求频率超限,请稍后再试', 'RATE_LIMIT', 429) } if (error.status === 500) { return new AIError('AI 服务暂时不可用', 'SERVICE_UNAVAILABLE', 500) } return new AIError('AI 请求失败', 'UNKNOWN_ERROR', 500) } ``` ### 错误重试机制 ```typescript title="lib/ai/retry.ts" theme={null} export async function withRetry( fn: () => Promise, maxRetries: number = 3, delay: number = 1000 ): Promise { let lastError: Error for (let i = 0; i < maxRetries; i++) { try { return await fn() } catch (error) { lastError = error as Error if (i === maxRetries - 1) break // 指数退避 await new Promise(resolve => setTimeout(resolve, delay * Math.pow(2, i))) } } throw lastError! } ``` ## 对话历史保存 ```typescript title="lib/ai/conversation.ts" theme={null} export async function saveConversation( userId: string, messages: any[], response: string ) { // 保存对话到数据库 await prisma.chat.create({ data: { userId, title: messages[0]?.content?.slice(0, 50) || '新对话', messages: { create: [ ...messages.map(msg => ({ content: msg.content, role: msg.role.toUpperCase(), userId })), { content: response, role: 'ASSISTANT', userId } ] } } }) } ``` 建议为不同用途配置不同的 AI 模型,如客服使用 GPT-4,创作使用 Claude。 ## 最佳实践 1. **成本控制** - 设置用量限制和积分系统 2. **错误处理** - 优雅处理 API 失败和超时 3. **缓存策略** - 缓存常见问题的回答 4. **监控告警** - 监控 API 使用量和错误率 查看各 AI 提供商的官方文档了解更多模型参数和限制。 # 用户认证 Source: https://codofly.mintlify.app/features/authentication 使用 NextAuth.js 实现用户认证和授权 # 用户认证 Codofly Template 使用 NextAuth.js 5 提供完整的用户认证解决方案。 ## NextAuth.js 配置 ### 基础配置 ```typescript title="lib/auth.ts" theme={null} import NextAuth from 'next-auth' import GitHub from 'next-auth/providers/github' import Google from 'next-auth/providers/google' import Credentials from 'next-auth/providers/credentials' import { PrismaAdapter } from '@auth/prisma-adapter' import { prisma } from '@/lib/prisma' export const { handlers, auth, signIn, signOut } = NextAuth({ adapter: PrismaAdapter(prisma), providers: [ GitHub({ clientId: process.env.GITHUB_CLIENT_ID, clientSecret: process.env.GITHUB_CLIENT_SECRET, }), Google({ clientId: process.env.GOOGLE_CLIENT_ID, clientSecret: process.env.GOOGLE_CLIENT_SECRET, }), Credentials({ name: 'credentials', credentials: { email: { label: 'Email', type: 'email' }, password: { label: 'Password', type: 'password' } }, async authorize(credentials) { if (!credentials?.email || !credentials?.password) return null // 验证用户凭据的逻辑 const user = await verifyUser(credentials.email, credentials.password) return user || null } }) ], session: { strategy: 'jwt' }, callbacks: { async jwt({ token, user }) { if (user) { token.role = user.role } return token }, async session({ session, token }) { if (token) { session.user.id = token.sub session.user.role = token.role } return session } }, pages: { signIn: '/auth/signin', signUp: '/auth/signup' } }) ``` ### Route Handler ```typescript title="app/api/auth/[...nextauth]/route.ts" theme={null} import { handlers } from '@/lib/auth' export const { GET, POST } = handlers ``` ## 登录方式配置 ### GitHub 登录 ```bash theme={null} # 环境变量 GITHUB_CLIENT_ID=your_github_client_id GITHUB_CLIENT_SECRET=your_github_client_secret ``` 在 GitHub Developer Settings 中: 1. 创建 OAuth App 2. 设置回调 URL: `http://localhost:3000/api/auth/callback/github` ### Google 登录 ```bash theme={null} # 环境变量 GOOGLE_CLIENT_ID=your_google_client_id GOOGLE_CLIENT_SECRET=your_google_client_secret ``` 在 Google Cloud Console 中: 1. 创建 OAuth 2.0 客户端 2. 设置回调 URL: `http://localhost:3000/api/auth/callback/google` ### 邮箱密码登录 ```typescript title="lib/auth-utils.ts" theme={null} import bcrypt from 'bcryptjs' import { prisma } from '@/lib/prisma' export async function verifyUser(email: string, password: string) { const user = await prisma.user.findUnique({ where: { email } }) if (!user || !user.password) return null const isValid = await bcrypt.compare(password, user.password) if (!isValid) return null return { id: user.id, email: user.email, name: user.name, role: user.role } } export async function createUser(email: string, password: string, name: string) { const hashedPassword = await bcrypt.hash(password, 12) return await prisma.user.create({ data: { email, password: hashedPassword, name } }) } ``` ## 会话管理 ### 获取会话信息 ```typescript title="components/user-profile.tsx" theme={null} import { useSession } from 'next-auth/react' export function UserProfile() { const { data: session, status } = useSession() if (status === 'loading') return 加载中... if (status === 'unauthenticated') return 未登录 return ( 欢迎,{session?.user?.name} 邮箱:{session?.user?.email} ) } ``` ### 服务端获取会话 ```typescript title="app/dashboard/page.tsx" theme={null} import { auth } from '@/lib/auth' import { redirect } from 'next/navigation' export default async function DashboardPage() { const session = await auth() if (!session) { redirect('/auth/signin') } return ( 欢迎,{session.user?.name} ) } ``` ## 权限控制 ### 页面级权限保护 ```typescript title="components/auth/protected-page.tsx" theme={null} import { auth } from '@/lib/auth' import { redirect } from 'next/navigation' export async function ProtectedPage({ children, requiredRole = 'USER' }: { children: React.ReactNode requiredRole?: 'USER' | 'ADMIN' }) { const session = await auth() if (!session) { redirect('/auth/signin') } if (requiredRole === 'ADMIN' && session.user?.role !== 'ADMIN') { redirect('/unauthorized') } return <>{children}> } ``` ### API 路由保护 ```typescript title="lib/auth-middleware.ts" theme={null} import { auth } from '@/lib/auth' import { NextRequest, NextResponse } from 'next/server' export async function withAuth(handler: Function) { return async (request: NextRequest, context: any) => { const session = await auth() if (!session) { return NextResponse.json( { error: '未授权访问' }, { status: 401 } ) } // 将用户信息添加到请求上下文 context.user = session.user return handler(request, context) } } ``` ### 组件级权限 ```typescript title="components/auth/role-guard.tsx" theme={null} 'use client' import { useSession } from 'next-auth/react' interface RoleGuardProps { allowedRoles: string[] children: React.ReactNode fallback?: React.ReactNode } export function RoleGuard({ allowedRoles, children, fallback }: RoleGuardProps) { const { data: session } = useSession() if (!session?.user?.role || !allowedRoles.includes(session.user.role)) { return fallback || 权限不足 } return <>{children}> } ``` ## 用户信息管理 ### 更新用户信息 ```typescript title="app/api/user/profile/route.ts" theme={null} import { auth } from '@/lib/auth' import { prisma } from '@/lib/prisma' export async function PUT(request: Request) { const session = await auth() if (!session) { return NextResponse.json({ error: '未授权' }, { status: 401 }) } const { name, avatar } = await request.json() const user = await prisma.user.update({ where: { id: session.user.id }, data: { name, avatar } }) return NextResponse.json({ user }) } ``` ### 客户端更新 ```typescript title="components/profile-form.tsx" theme={null} 'use client' import { useState } from 'react' import { useSession } from 'next-auth/react' export function ProfileForm() { const { data: session, update } = useSession() const [name, setName] = useState(session?.user?.name || '') const handleSubmit = async (e: React.FormEvent) => { e.preventDefault() const response = await fetch('/api/user/profile', { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name }) }) if (response.ok) { // 更新客户端会话 await update({ name }) } } return ( setName(e.target.value)} placeholder="姓名" /> 保存 ) } ``` ## 登出功能 ### 客户端登出 ```typescript title="components/logout-button.tsx" theme={null} 'use client' import { signOut } from 'next-auth/react' export function LogoutButton() { return ( signOut({ callbackUrl: '/' })} className="text-red-600 hover:text-red-800" > 登出 ) } ``` ### 服务端登出 ```typescript title="app/api/auth/signout/route.ts" theme={null} import { signOut } from '@/lib/auth' export async function POST() { await signOut({ redirectTo: '/' }) } ``` ## 中间件配置 ```typescript title="middleware.ts" theme={null} import { auth } from '@/lib/auth' import { NextResponse } from 'next/server' export default auth((req) => { const { pathname } = req.nextUrl // 保护的路由 if (pathname.startsWith('/dashboard')) { if (!req.auth) { return NextResponse.redirect(new URL('/auth/signin', req.url)) } } // 管理员路由 if (pathname.startsWith('/admin')) { if (!req.auth || req.auth.user?.role !== 'ADMIN') { return NextResponse.redirect(new URL('/unauthorized', req.url)) } } return NextResponse.next() }) export const config = { matcher: ['/((?!api|_next/static|_next/image|favicon.ico).*)'] } ``` ## 会话提供者 ```typescript title="components/providers/session-provider.tsx" theme={null} 'use client' import { SessionProvider } from 'next-auth/react' export function Providers({ children }: { children: React.ReactNode }) { return ( {children} ) } ``` 确保在根布局中包装 SessionProvider 以在整个应用中使用会话功能。 ## 最佳实践 1. **安全的密钥管理** - 使用强随机字符串作为 NEXTAUTH\_SECRET 2. **会话过期** - 合理设置会话过期时间 3. **权限细分** - 根据业务需求设计权限系统 4. **错误处理** - 优雅处理认证失败情况 查看 [NextAuth.js 文档](https://next-auth.js.org) 了解更多配置选项。 # 数据库功能 Source: https://codofly.mintlify.app/features/database 使用 Prisma ORM 管理 Codofly Template 的数据库 # 数据库功能 Codofly Template 使用 Prisma ORM 来管理数据库操作,提供类型安全的数据访问。 ## Prisma Schema 结构 ### 核心数据模型 ```prisma title="prisma/schema.prisma" theme={null} generator client { provider = "prisma-client-js" } datasource db { provider = "postgresql" url = env("DATABASE_URL") } model User { id String @id @default(cuid()) email String @unique name String? avatar String? role Role @default(USER) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt // 关联关系 chats Chat[] teams TeamMember[] messages Message[] @@map("users") } model Team { id String @id @default(cuid()) name String description String? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt // 关联关系 members TeamMember[] chats Chat[] @@map("teams") } model Chat { id String @id @default(cuid()) title String userId String teamId String? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt // 关联关系 user User @relation(fields: [userId], references: [id], onDelete: Cascade) team Team? @relation(fields: [teamId], references: [id]) messages Message[] @@map("chats") } model Message { id String @id @default(cuid()) content String role MessageRole chatId String userId String createdAt DateTime @default(now()) // 关联关系 chat Chat @relation(fields: [chatId], references: [id], onDelete: Cascade) user User @relation(fields: [userId], references: [id]) @@map("messages") } enum Role { USER ADMIN } enum MessageRole { USER ASSISTANT } ``` ## 基础数据操作 ### 用户操作 ```typescript title="lib/db/users.ts" theme={null} import { prisma } from '@/lib/prisma' // 创建用户 export async function createUser(data: { email: string name: string }) { return await prisma.user.create({ data }) } // 查询用户 export async function getUser(id: string) { return await prisma.user.findUnique({ where: { id }, include: { chats: true, teams: true } }) } // 更新用户 export async function updateUser(id: string, data: any) { return await prisma.user.update({ where: { id }, data }) } // 删除用户 export async function deleteUser(id: string) { return await prisma.user.delete({ where: { id } }) } ``` ### 聊天操作 ```typescript title="lib/db/chats.ts" theme={null} // 创建聊天 export async function createChat(data: { title: string userId: string teamId?: string }) { return await prisma.chat.create({ data, include: { messages: true } }) } // 获取用户聊天列表 export async function getUserChats(userId: string) { return await prisma.chat.findMany({ where: { userId }, include: { messages: { orderBy: { createdAt: 'asc' } } }, orderBy: { updatedAt: 'desc' } }) } // 添加消息 export async function addMessage(data: { content: string role: 'USER' | 'ASSISTANT' chatId: string userId: string }) { return await prisma.message.create({ data }) } ``` ## 关系查询 ### 复杂查询示例 ```typescript title="lib/db/advanced-queries.ts" theme={null} // 获取团队成员及其聊天 export async function getTeamWithChats(teamId: string) { return await prisma.team.findUnique({ where: { id: teamId }, include: { members: { include: { user: true } }, chats: { include: { messages: { take: 10, orderBy: { createdAt: 'desc' } } } } } }) } // 搜索聊天记录 export async function searchMessages(userId: string, query: string) { return await prisma.message.findMany({ where: { AND: [ { userId }, { content: { contains: query, mode: 'insensitive' } } ] }, include: { chat: true }, orderBy: { createdAt: 'desc' } }) } ``` ### 事务操作 ```typescript title="lib/db/transactions.ts" theme={null} // 创建聊天并添加首条消息 export async function createChatWithMessage(data: { title: string userId: string message: string }) { return await prisma.$transaction(async (tx) => { const chat = await tx.chat.create({ data: { title: data.title, userId: data.userId } }) await tx.message.create({ data: { content: data.message, role: 'USER', chatId: chat.id, userId: data.userId } }) return chat }) } ``` ## 数据迁移 ### 创建迁移 ```bash theme={null} # 创建新迁移 npx prisma migrate dev --name add_team_feature # 应用迁移到生产环境 npx prisma migrate deploy # 重置数据库(开发环境) npx prisma migrate reset ``` ### 迁移文件示例 ```sql title="migrations/001_initial/migration.sql" theme={null} -- CreateEnum CREATE TYPE "Role" AS ENUM ('USER', 'ADMIN'); -- CreateTable CREATE TABLE "users" ( "id" TEXT NOT NULL, "email" TEXT NOT NULL, "name" TEXT, "role" "Role" NOT NULL DEFAULT 'USER', "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, "updatedAt" TIMESTAMP(3) NOT NULL, CONSTRAINT "users_pkey" PRIMARY KEY ("id") ); -- CreateIndex CREATE UNIQUE INDEX "users_email_key" ON "users"("email"); ``` ## 数据种子 ### 种子文件 ```typescript title="prisma/seed.ts" theme={null} import { PrismaClient } from '@prisma/client' const prisma = new PrismaClient() async function main() { // 创建测试用户 const user = await prisma.user.upsert({ where: { email: 'admin@codofly.com' }, update: {}, create: { email: 'admin@codofly.com', name: 'Admin User', role: 'ADMIN' } }) // 创建测试聊天 await prisma.chat.create({ data: { title: '第一个聊天', userId: user.id, messages: { create: [ { content: '你好!', role: 'USER', userId: user.id }, { content: '你好!我是 AI 助手。', role: 'ASSISTANT', userId: user.id } ] } } }) console.log('数据种子创建完成') } main() .catch((e) => { console.error(e) process.exit(1) }) .finally(async () => { await prisma.$disconnect() }) ``` ### 运行种子 ```bash theme={null} # 运行种子文件 npx prisma db seed # 或者在 package.json 中配置 { "prisma": { "seed": "tsx prisma/seed.ts" } } ``` ## 最佳实践 ### 1. 连接管理 ```typescript title="lib/prisma.ts" theme={null} import { PrismaClient } from '@prisma/client' const globalForPrisma = globalThis as unknown as { prisma: PrismaClient | undefined } export const prisma = globalForPrisma.prisma ?? new PrismaClient() if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma ``` ### 2. 错误处理 ```typescript theme={null} try { const user = await prisma.user.create({ data }) } catch (error) { if (error.code === 'P2002') { // 唯一约束违反 throw new Error('邮箱已存在') } throw error } ``` ### 3. 性能优化 ```typescript theme={null} // 使用选择字段 const users = await prisma.user.findMany({ select: { id: true, name: true, email: true } }) // 分页查询 const users = await prisma.user.findMany({ skip: (page - 1) * limit, take: limit }) ``` 查看 [Prisma 官方文档](https://www.prisma.io/docs) 了解更多高级功能。 # 国际化 Source: https://codofly.mintlify.app/features/i18n 使用 next-intl 实现多语言支持 # 国际化 Codofly Template 使用 next-intl 提供完整的国际化支持,目前支持英文和简体中文。 ## next-intl 配置 ### 基础配置 ```typescript title="i18n.ts" theme={null} import { notFound } from 'next/navigation' import { getRequestConfig } from 'next-intl/server' // 支持的语言列表 export const locales = ['en', 'zh'] as const export type Locale = typeof locales[number] export default getRequestConfig(async ({ locale }) => { // 验证语言是否支持 if (!locales.includes(locale as any)) notFound() return { messages: (await import(`./messages/${locale}.json`)).default } }) ``` ### 中间件配置 ```typescript title="middleware.ts" theme={null} import createMiddleware from 'next-intl/middleware' import { locales } from './i18n' export default createMiddleware({ locales, defaultLocale: 'en', localePrefix: 'always' }) export const config = { matcher: ['/((?!api|_next|_vercel|.*\\..*).*)'] } ``` ## 翻译文件管理 ### 英文翻译 ```json title="messages/en.json" theme={null} { "Common": { "save": "Save", "cancel": "Cancel", "delete": "Delete", "edit": "Edit", "loading": "Loading...", "error": "An error occurred" }, "Navigation": { "home": "Home", "dashboard": "Dashboard", "teams": "Teams", "settings": "Settings", "signIn": "Sign In", "signOut": "Sign Out" }, "HomePage": { "title": "Build AI SaaS Apps Fast", "subtitle": "Everything you need to launch your AI-powered SaaS", "getStarted": "Get Started", "learnMore": "Learn More" }, "Features": { "ai": { "title": "Multi-AI Integration", "description": "Support for OpenAI, Anthropic, and Google models" }, "auth": { "title": "User Authentication", "description": "Complete auth system with social login" }, "teams": { "title": "Team Collaboration", "description": "Built-in team management and sharing" } } } ``` ### 中文翻译 ```json title="messages/zh.json" theme={null} { "Common": { "save": "保存", "cancel": "取消", "delete": "删除", "edit": "编辑", "loading": "加载中...", "error": "发生错误" }, "Navigation": { "home": "首页", "dashboard": "仪表盘", "teams": "团队", "settings": "设置", "signIn": "登录", "signOut": "退出" }, "HomePage": { "title": "快速构建 AI SaaS 应用", "subtitle": "启动 AI 驱动的 SaaS 所需的一切", "getStarted": "开始使用", "learnMore": "了解更多" }, "Features": { "ai": { "title": "多 AI 模型集成", "description": "支持 OpenAI、Anthropic 和 Google 模型" }, "auth": { "title": "用户认证", "description": "完整的认证系统,支持社交登录" }, "teams": { "title": "团队协作", "description": "内置团队管理和共享功能" } } } ``` ## 多语言路由 ### 根布局 ```typescript title="app/[locale]/layout.tsx" theme={null} import { NextIntlClientProvider } from 'next-intl' import { getMessages } from 'next-intl/server' import { setRequestLocale } from 'next-intl/server' import { locales } from '@/i18n' export function generateStaticParams() { return locales.map((locale) => ({ locale })) } interface Props { children: React.ReactNode params: { locale: string } } export default async function LocaleLayout({ children, params: { locale } }: Props) { // 启用静态渲染 setRequestLocale(locale) const messages = await getMessages() return ( {children} ) } ``` ### 页面国际化 ```typescript title="app/[locale]/page.tsx" theme={null} import { useTranslations } from 'next-intl' import { setRequestLocale } from 'next-intl/server' interface Props { params: { locale: string } } export default function HomePage({ params: { locale } }: Props) { setRequestLocale(locale) const t = useTranslations('HomePage') return ( {t('title')} {t('subtitle')} {t('getStarted')} {t('learnMore')} ) } ``` ## 语言切换 ### 语言切换组件 ```typescript title="components/language-switcher.tsx" theme={null} 'use client' import { useLocale } from 'next-intl' import { usePathname, useRouter } from 'next/navigation' import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select' const languages = [ { code: 'en', name: 'English', flag: '🇺🇸' }, { code: 'zh', name: '简体中文', flag: '🇨🇳' } ] export function LanguageSwitcher() { const locale = useLocale() const router = useRouter() const pathname = usePathname() const handleLanguageChange = (newLocale: string) => { // 移除当前语言前缀 const pathWithoutLocale = pathname.replace(`/${locale}`, '') || '/' // 添加新语言前缀 const newPath = `/${newLocale}${pathWithoutLocale}` router.push(newPath) } return ( {languages.find(lang => lang.code === locale)?.flag}{' '} {languages.find(lang => lang.code === locale)?.name} {languages.map(language => ( {language.flag} {language.name} ))} ) } ``` ### 导航栏集成 ```typescript title="components/navbar.tsx" theme={null} import { useTranslations } from 'next-intl' import { LanguageSwitcher } from './language-switcher' import { Link } from '@/navigation' export function Navbar() { const t = useTranslations('Navigation') return ( {t('home')} {t('dashboard')} {t('teams')} {t('signIn')} ) } ``` ## 内容本地化 ### 日期和时间 ```typescript title="lib/date-formatter.ts" theme={null} import { useLocale } from 'next-intl' export function useDateFormatter() { const locale = useLocale() const formatDate = (date: Date) => { return new Intl.DateTimeFormat(locale, { year: 'numeric', month: 'long', day: 'numeric' }).format(date) } const formatDateTime = (date: Date) => { return new Intl.DateTimeFormat(locale, { year: 'numeric', month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' }).format(date) } return { formatDate, formatDateTime } } ``` ### 数字格式化 ```typescript title="components/price-display.tsx" theme={null} import { useLocale } from 'next-intl' interface PriceDisplayProps { amount: number currency?: string } export function PriceDisplay({ amount, currency = 'USD' }: PriceDisplayProps) { const locale = useLocale() const formatPrice = (amount: number, currency: string) => { return new Intl.NumberFormat(locale, { style: 'currency', currency: currency }).format(amount) } return {formatPrice(amount, currency)} } ``` ### 复数处理 ```typescript title="components/message-count.tsx" theme={null} import { useTranslations } from 'next-intl' interface MessageCountProps { count: number } export function MessageCount({ count }: MessageCountProps) { const t = useTranslations('Messages') return ( {t('count', { count })} ) } ``` 对应的翻译文件: ```json theme={null} { "Messages": { "count": { "0": "No messages", "1": "1 message", "other": "{count} messages" } } } ``` ## 动态导入翻译 ### 按需加载翻译 ```typescript title="lib/translations.ts" theme={null} export async function loadTranslations(locale: string, namespace: string) { try { const messages = await import(`@/messages/${locale}/${namespace}.json`) return messages.default } catch (error) { console.warn(`Failed to load ${namespace} translations for ${locale}`) return {} } } ``` ### 分离翻译文件 ``` messages/ ├── en/ │ ├── common.json │ ├── navigation.json │ ├── auth.json │ └── dashboard.json └── zh/ ├── common.json ├── navigation.json ├── auth.json └── dashboard.json ``` ## 添加新语言 ### 步骤 1. **添加语言到配置** ```typescript title="i18n.ts" theme={null} export const locales = ['en', 'zh', 'ja'] as const // 添加日语 ``` 2. **创建翻译文件** ```json title="messages/ja.json" theme={null} { "Common": { "save": "保存", "cancel": "キャンセル", "delete": "削除" } } ``` 3. **更新语言切换器** ```typescript title="components/language-switcher.tsx" theme={null} const languages = [ { code: 'en', name: 'English', flag: '🇺🇸' }, { code: 'zh', name: '简体中文', flag: '🇨🇳' }, { code: 'ja', name: '日本語', flag: '🇯🇵' } // 添加日语 ] ``` ## 服务端组件翻译 ### API 响应本地化 ```typescript title="app/api/messages/route.ts" theme={null} import { getTranslations } from 'next-intl/server' export async function POST(request: Request) { const { locale } = await request.json() const t = await getTranslations({ locale, namespace: 'API' }) try { // 处理逻辑 return NextResponse.json({ message: t('success') }) } catch (error) { return NextResponse.json({ error: t('error') }, { status: 500 }) } } ``` ## 最佳实践 ### 1. 翻译键命名 ```typescript theme={null} // ✅ 好的命名 "Auth.SignIn.title" "Dashboard.Stats.users" "Teams.Members.invite" // ❌ 避免的命名 "text1" "message" "button" ``` ### 2. 翻译文件结构 ```json theme={null} { "ComponentName": { "title": "标题", "description": "描述", "actions": { "save": "保存", "cancel": "取消" } } } ``` ### 3. 处理缺失翻译 ```typescript title="lib/fallback-translations.ts" theme={null} export function withFallback(key: string, fallback: string) { const t = useTranslations() try { return t(key) } catch { return fallback } } ``` 建议使用专业翻译工具如 Crowdin 或 Lokalise 来管理大型项目的翻译工作。 ## 性能优化 ### 翻译预加载 ```typescript title="app/[locale]/loading.tsx" theme={null} import { getMessages } from 'next-intl/server' export default async function Loading({ params: { locale } }) { // 预加载翻译 await getMessages() return Loading... } ``` ### 静态生成 ```typescript title="next.config.js" theme={null} /** @type {import('next').NextConfig} */ const nextConfig = { experimental: { ppr: true, // 启用部分预渲染 }, } module.exports = nextConfig ``` 查看 [next-intl 文档](https://next-intl-docs.vercel.app/) 了解更多高级功能。 # 支付集成 Source: https://codofly.mintlify.app/features/payment 使用 Stripe 实现订阅和支付功能 # 支付集成 Codofly Template 集成 Stripe 支付系统,支持订阅管理、一次性付款和积分充值。 ## Stripe 配置 ### 环境变量 ```bash theme={null} # Stripe 密钥 STRIPE_SECRET_KEY=sk_test_... STRIPE_PUBLISHABLE_KEY=pk_test_... STRIPE_WEBHOOK_SECRET=whsec_... # 产品和价格 ID STRIPE_PRICE_ID_BASIC=price_... STRIPE_PRICE_ID_PRO=price_... ``` ### 初始化 Stripe ```typescript title="lib/stripe.ts" theme={null} import Stripe from 'stripe' export const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, { apiVersion: '2024-06-20', typescript: true, }) // 客户端 Stripe export const stripePromise = loadStripe( process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY! ) ``` ## 订阅管理 ### 创建订阅 ```typescript title="app/api/stripe/create-subscription/route.ts" theme={null} import { auth } from '@/lib/auth' import { stripe } from '@/lib/stripe' import { prisma } from '@/lib/prisma' export async function POST(request: Request) { const session = await auth() if (!session) { return NextResponse.json({ error: '未授权' }, { status: 401 }) } const { priceId } = await request.json() try { // 创建或获取 Stripe 客户 let customer = await prisma.user.findUnique({ where: { id: session.user.id }, select: { stripeCustomerId: true } }) if (!customer?.stripeCustomerId) { const stripeCustomer = await stripe.customers.create({ email: session.user.email!, name: session.user.name!, }) await prisma.user.update({ where: { id: session.user.id }, data: { stripeCustomerId: stripeCustomer.id } }) customer = { stripeCustomerId: stripeCustomer.id } } // 创建订阅 const subscription = await stripe.subscriptions.create({ customer: customer.stripeCustomerId!, items: [{ price: priceId }], payment_behavior: 'default_incomplete', payment_settings: { save_default_payment_method: 'on_subscription' }, expand: ['latest_invoice.payment_intent'], }) return NextResponse.json({ subscriptionId: subscription.id, clientSecret: subscription.latest_invoice?.payment_intent?.client_secret, }) } catch (error) { return NextResponse.json({ error: '创建订阅失败' }, { status: 500 }) } } ``` ### 订阅组件 ```typescript title="components/subscription/subscription-plans.tsx" theme={null} 'use client' import { useState } from 'react' import { Button } from '@/components/ui/button' import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' const plans = [ { name: 'Basic', price: '$9.99', priceId: 'price_basic', features: ['100 AI 对话', '基础功能', '邮件支持'] }, { name: 'Pro', price: '$29.99', priceId: 'price_pro', features: ['无限 AI 对话', '高级功能', '优先支持'] } ] export function SubscriptionPlans() { const [loading, setLoading] = useState(null) const handleSubscribe = async (priceId: string) => { setLoading(priceId) try { const response = await fetch('/api/stripe/create-subscription', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ priceId }) }) const { clientSecret } = await response.json() // 重定向到 Stripe Checkout 或处理支付 if (clientSecret) { // 处理支付确认 } } catch (error) { console.error('订阅失败:', error) } finally { setLoading(null) } } return ( {plans.map((plan) => ( {plan.name} {plan.price}/月 {plan.features.map((feature) => ( ✓ {feature} ))} handleSubscribe(plan.priceId)} disabled={loading === plan.priceId} className="w-full" > {loading === plan.priceId ? '处理中...' : '选择计划'} ))} ) } ``` ## 一次性付款 ### 创建支付意图 ```typescript title="app/api/stripe/create-payment-intent/route.ts" theme={null} export async function POST(request: Request) { const { amount, currency = 'usd' } = await request.json() try { const paymentIntent = await stripe.paymentIntents.create({ amount: amount * 100, // 转换为分 currency, automatic_payment_methods: { enabled: true }, }) return NextResponse.json({ clientSecret: paymentIntent.client_secret, }) } catch (error) { return NextResponse.json({ error: '创建支付失败' }, { status: 500 }) } } ``` ### 支付表单 ```typescript title="components/payment/payment-form.tsx" theme={null} 'use client' import { useState } from 'react' import { Elements, PaymentElement, useStripe, useElements } from '@stripe/react-stripe-js' import { stripePromise } from '@/lib/stripe' function CheckoutForm({ clientSecret }: { clientSecret: string }) { const stripe = useStripe() const elements = useElements() const [isLoading, setIsLoading] = useState(false) const handleSubmit = async (e: React.FormEvent) => { e.preventDefault() if (!stripe || !elements) return setIsLoading(true) const { error } = await stripe.confirmPayment({ elements, confirmParams: { return_url: `${window.location.origin}/payment/success`, }, }) if (error) { console.error('支付失败:', error) } setIsLoading(false) } return ( {isLoading ? '处理中...' : '支付'} ) } export function PaymentForm({ clientSecret }: { clientSecret: string }) { return ( ) } ``` ## 积分系统 ### 积分充值 ```typescript title="app/api/credits/purchase/route.ts" theme={null} export async function POST(request: Request) { const session = await auth() if (!session) { return NextResponse.json({ error: '未授权' }, { status: 401 }) } const { amount, credits } = await request.json() try { // 创建支付意图 const paymentIntent = await stripe.paymentIntents.create({ amount: amount * 100, currency: 'usd', metadata: { userId: session.user.id, credits: credits.toString(), type: 'credit_purchase' } }) return NextResponse.json({ clientSecret: paymentIntent.client_secret, }) } catch (error) { return NextResponse.json({ error: '创建支付失败' }, { status: 500 }) } } ``` ### 积分消费 ```typescript title="lib/credits.ts" theme={null} import { prisma } from '@/lib/prisma' export async function consumeCredits(userId: string, amount: number) { const user = await prisma.user.findUnique({ where: { id: userId }, select: { credits: true } }) if (!user || user.credits < amount) { throw new Error('积分不足') } // 扣除积分 await prisma.user.update({ where: { id: userId }, data: { credits: { decrement: amount } } }) // 记录消费记录 await prisma.creditTransaction.create({ data: { userId, amount: -amount, type: 'CONSUMPTION', description: 'AI 对话消费' } }) } export async function addCredits(userId: string, amount: number) { await prisma.user.update({ where: { id: userId }, data: { credits: { increment: amount } } }) await prisma.creditTransaction.create({ data: { userId, amount, type: 'PURCHASE', description: '积分充值' } }) } ``` ## Webhook 处理 ### Stripe Webhook ```typescript title="app/api/webhooks/stripe/route.ts" theme={null} import { headers } from 'next/headers' import { stripe } from '@/lib/stripe' export async function POST(request: Request) { const body = await request.text() const signature = headers().get('stripe-signature')! let event: Stripe.Event try { event = stripe.webhooks.constructEvent( body, signature, process.env.STRIPE_WEBHOOK_SECRET! ) } catch (err) { return NextResponse.json( { error: 'Webhook signature verification failed' }, { status: 400 } ) } try { switch (event.type) { case 'payment_intent.succeeded': await handlePaymentSuccess(event.data.object as Stripe.PaymentIntent) break case 'customer.subscription.created': await handleSubscriptionCreated(event.data.object as Stripe.Subscription) break case 'customer.subscription.deleted': await handleSubscriptionCanceled(event.data.object as Stripe.Subscription) break } return NextResponse.json({ received: true }) } catch (error) { return NextResponse.json( { error: 'Webhook handler failed' }, { status: 500 } ) } } async function handlePaymentSuccess(paymentIntent: Stripe.PaymentIntent) { const { userId, credits, type } = paymentIntent.metadata if (type === 'credit_purchase' && userId && credits) { await addCredits(userId, parseInt(credits)) } } ``` ## 账单管理 ### 获取账单历史 ```typescript title="app/api/billing/history/route.ts" theme={null} export async function GET() { const session = await auth() if (!session) { return NextResponse.json({ error: '未授权' }, { status: 401 }) } const user = await prisma.user.findUnique({ where: { id: session.user.id }, select: { stripeCustomerId: true } }) if (!user?.stripeCustomerId) { return NextResponse.json({ invoices: [] }) } const invoices = await stripe.invoices.list({ customer: user.stripeCustomerId, limit: 10, }) return NextResponse.json({ invoices: invoices.data }) } ``` ### 账单组件 ```typescript title="components/billing/billing-history.tsx" theme={null} 'use client' import { useEffect, useState } from 'react' export function BillingHistory() { const [invoices, setInvoices] = useState([]) useEffect(() => { fetch('/api/billing/history') .then(res => res.json()) .then(data => setInvoices(data.invoices)) }, []) return ( 账单历史 {invoices.map((invoice: any) => ( #{invoice.number} ${(invoice.amount_paid / 100).toFixed(2)} {new Date(invoice.created * 1000).toLocaleDateString()} ))} ) } ``` ## 退款处理 ### 创建退款 ```typescript title="app/api/stripe/refund/route.ts" theme={null} export async function POST(request: Request) { const { paymentIntentId, amount } = await request.json() try { const refund = await stripe.refunds.create({ payment_intent: paymentIntentId, amount: amount ? amount * 100 : undefined, // 部分退款或全额退款 }) return NextResponse.json({ refund }) } catch (error) { return NextResponse.json({ error: '退款失败' }, { status: 500 }) } } ``` 确保在生产环境中使用真实的 Stripe 密钥,并正确配置 Webhook 端点。 ## 最佳实践 1. **安全性** - 永远不要在客户端暴露 Stripe 密钥 2. **Webhook** - 使用 Webhook 处理支付状态更新 3. **错误处理** - 优雅处理支付失败情况 4. **测试** - 使用 Stripe 测试模式进行开发 查看 [Stripe 文档](https://stripe.com/docs) 了解更多支付功能。 # Codofly Source: https://codofly.mintlify.app/introduction codofly ai template 介绍 ## 模板简介 Codofly 是一个企业级 AI SaaS 应用开发模板,基于 Next.js 15 构建。它集成了用户认证、支付订阅、AI 功能、团队协作等完整的企业级功能,让您可以快速启动 AI 产品开发,专注于业务逻辑而非基础设施搭建。 Codofly 不仅是一个模板,更是一个完整的 AI SaaS 应用架构参考,为开发类似产品提供了坚实的基础。 ## 主要特性 ### 🤖 多 AI 模型集成 * 支持 OpenAI (GPT-4o、GPT-4.1)、Anthropic (Claude 3.7、Claude 3.5)、Google (Gemini 2.0) 等多种模型 * 流式响应实现,确保实时输出 * 按模型动态计费系统 ```typescript theme={null} export function getModelClient(model: LLMModel, config: LLMModelConfig) { const { id: modelNameString, providerId } = model; const { apiKey = process.env.OPENAI_API_KEY, baseURL = process.env.OPENAI_API_BASE_URL, } = config; const providerConfigs = { openai: () => createOpenAI({ apiKey, baseURL })(modelNameString), // 其他模型... }; // ... } ``` ### 💳 完善的支付系统 * Stripe 订阅管理和一次性付款 * 精确按模型使用量计费的积分系统 * 完整的订单和积分变动记录 ### 🔐 企业级认证系统 * 社交账号登录 (GitHub, Google) * 邮箱验证和登录 * JWT 会话管理 * 角色和权限控制 ### 👥 团队协作功能 * 团队创建和管理 * 成员邀请和权限控制 * 团队资源共享(如聊天记录) ### 🌍 完整国际化支持 * 英文和简体中文 * 路由级别国际化 * 用户界面和内容翻译 ## 技术栈 使用 App Router 的现代 React 框架 强类型支持,提高代码质量 类型安全的数据库访问 现代认证解决方案 成熟的支付解决方案 实用的 CSS 框架 ## 演示 您可以访问我们的在线演示站点来体验 Codofly Template 的功能: [在线演示](https://codofly.com) ## 系统要求 使用 Codofly Template 开发需要以下环境: ```bash theme={null} Node.js >= 18.0.0 pnpm >= 8.0.0 PostgreSQL >= 14.0 ``` 确保您的开发环境满足以上要求,以避免可能出现的兼容性问题。 ## 快速开始 通过以下步骤快速启动项目: ```bash theme={null} # 克隆项目 git clone https://github.com/codofly/codofly cd codofly # 安装依赖 pnpm install # 设置环境变量 cp .env.example .env.local # 初始化数据库 npx prisma migrate dev # 启动开发服务器 pnpm dev ``` 现在您可以访问 [http://localhost:3000](http://localhost:3000) 查看应用。 查看 [快速开始](/quickstart) 文档获取更详细的安装和配置说明。 # 快速开始 Source: https://codofly.mintlify.app/quick-start 五分钟内完成 Codofly Template 的安装与配置 # 快速开始 本指南将帮助您快速上手 Codofly。 ## 环境准备 在开始之前,请确保您的系统满足以下要求: 推荐使用 LTS 版本 高效的包管理工具 关系型数据库 从 [Node.js 官网](https://nodejs.org/) 下载并安装最新的 LTS 版本。 ```bash theme={null} # 验证安装 node -v # 应显示 v18.x.x 或更高版本 ``` 按照 [pnpm 官方文档](https://pnpm.io/installation) 安装。 ```bash theme={null} # 使用 npm 安装 pnpm npm install -g pnpm # 验证安装 pnpm -v # 应显示 8.x.x 或更高版本 ``` 从 [PostgreSQL 官网](https://www.postgresql.org/download/) 下载并安装。 也可以使用 Docker 容器或云数据库服务如 Neon、Supabase 或 Vercel Postgres。 ## 获取模板代码 ```bash Git Clone theme={null} git clone https://github.com/codofly/codofly.git cd codofly ``` ```bash 下载 ZIP theme={null} # 下载 ZIP 文件 curl -L https://github.com/codofly/codofly/archive/refs/heads/main.zip -o codofly.zip # 解压文件 unzip codofly.zip # 进入项目目录 cd codofly-main ``` ## 安装依赖 ```bash theme={null} # 在项目根目录下运行 pnpm install ``` 请务必使用 pnpm 而不是 npm 或 yarn,以确保与 lockfile 一致,避免依赖问题。 ## 环境变量配置 ```bash theme={null} cp .env.example .env.local ``` 使用您喜欢的编辑器打开 `.env.local` 文件并配置以下关键变量: ```env theme={null} # PostgreSQL 连接字符串 DATABASE_URL=postgresql://username:password@localhost:5432/codofly ``` 将 `username`、`password` 替换为您的 PostgreSQL 用户名和密码。 ```env theme={null} # NextAuth 密钥和 URL NEXTAUTH_SECRET=your_random_secret_key NEXTAUTH_URL=http://localhost:3000 # 第三方登录(可选) GITHUB_ID=your_github_oauth_id GITHUB_SECRET=your_github_oauth_secret GOOGLE_ID=your_google_oauth_id GOOGLE_SECRET=your_google_oauth_secret ``` 生成随机密钥: ```bash theme={null} openssl rand -base64 32 ``` ```env theme={null} # Stripe 配置 STRIPE_SECRET_KEY=sk_test_your_stripe_secret_key STRIPE_WEBHOOK_SECRET=whsec_your_stripe_webhook_secret NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_your_stripe_publishable_key ``` 从 [Stripe Dashboard](https://dashboard.stripe.com/apikeys) 获取密钥。 ```env theme={null} # OpenAI 配置 OPENAI_API_KEY=your_openai_api_key # 其他 AI 提供商(可选) ANTHROPIC_API_KEY=your_anthropic_api_key GOOGLE_AI_KEY=your_google_ai_key ``` ```env theme={null} # 邮件服务 RESEND_API_KEY=your_resend_api_key EMAIL_FROM=noreply@yourdomain.com ``` 环境变量配置是项目正常运行的关键!确保所有必需的变量都已正确配置,特别是数据库连接和认证密钥。 ## 数据库初始化 运行以下命令初始化数据库模式: ```bash theme={null} npx prisma migrate dev ``` 这将创建所有必要的数据库表并设置初始数据。 如果您更改了数据库模型(schema.prisma),再次运行此命令可以更新数据库结构。 ## 本地运行 启动开发服务器: ```bash theme={null} pnpm dev ``` 如果一切配置正确,服务器将在 [http://localhost:3000](http://localhost:3000) 启动,并且控制台中不会显示错误。 ## 访问验证 打开浏览器并访问: ``` http://localhost:3000 ``` 您应该能看到 Codofly Template 的主页。尝试以下功能验证安装: 点击右上角的"登录"按钮,创建一个新账户或使用第三方登录。 登录后,您将被重定向到工作区,可以开始使用 AI 聊天功能。 创建一个新的聊天,选择一个 AI 模型,并发送一条测试消息。 确保您已配置了相应的 AI API 密钥,否则会收到错误响应。 ## 后续步骤 成功安装并运行 Codofly Template 后,您可以: 了解模板的所有功能,包括团队协作、订阅管理等。 根据您的业务需求修改和扩展模板功能。 在 Vercel、AWS 或其他云平台上部署您的应用。 查看[开发指南](/development)获取更多关于如何自定义和扩展模板的信息。
卡片内容
{trend.isPositive ? '+' : ''}{trend.value}% 相比上月
{product.description}
用户 ID: {params.id}
文章 ID: {params.postId}
{t('description')}
这是新功能页面的内容
{t("description")}
欢迎,{session?.user?.name}
邮箱:{session?.user?.email}
{t('subtitle')}