# Next.js API Routes + PostgreSQL 实施步骤 ## 第一步:安装依赖 ```bash # 数据库和 ORM pnpm add @prisma/client pnpm add -D prisma # 认证 pnpm add next-auth@beta pnpm add bcryptjs pnpm add -D @types/bcryptjs # 数据验证 pnpm add zod ``` ## 第二步:初始化 Prisma ```bash npx prisma init ``` 这会创建: - `prisma/schema.prisma` - 数据库模型定义 - `.env` - 环境变量文件 ## 第三步:配置数据库连接 ### 选择数据库提供商 **推荐选项:** 1. **Vercel Postgres**(推荐) - 与 Vercel 部署完美集成 - 免费额度:60小时计算时间/月 - 自动备份 2. **Neon** - 无服务器 PostgreSQL - 免费额度:3GB 存储 - 自动扩展 3. **Supabase**(仅用数据库) - 免费额度:500MB 数据库 - 自动备份 ### 配置 `.env` ```env # 数据库连接(示例使用 Vercel Postgres) DATABASE_URL="postgres://username:password@host:5432/database" # NextAuth 配置 NEXTAUTH_URL="http://localhost:3000" NEXTAUTH_SECRET="your-secret-key-here-generate-with-openssl-rand-base64-32" # 可选:邮件服务(用于邀请协作者) SMTP_HOST="smtp.gmail.com" SMTP_PORT="587" SMTP_USER="your-email@gmail.com" SMTP_PASSWORD="your-app-password" ``` ## 第四步:定义数据库模型 编辑 `prisma/schema.prisma`: ```prisma 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? password String avatar String? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt // 关系 ownedTrees FamilyTree[] @relation("TreeOwner") collaborations TreeCollaborator[] activityLogs ActivityLog[] } // 家族树表 model FamilyTree { id String @id @default(cuid()) name String description String? rootMemberId String? ownerId String createdAt DateTime @default(now()) updatedAt DateTime @updatedAt // 关系 owner User @relation("TreeOwner", fields: [ownerId], references: [id], onDelete: Cascade) members FamilyMember[] collaborators TreeCollaborator[] activityLogs ActivityLog[] @@index([ownerId]) } // 协作者表 model TreeCollaborator { id String @id @default(cuid()) treeId String userId String role Role @default(VIEWER) invitedBy String invitedAt DateTime @default(now()) // 关系 tree FamilyTree @relation(fields: [treeId], references: [id], onDelete: Cascade) user User @relation(fields: [userId], references: [id], onDelete: Cascade) @@unique([treeId, userId]) @@index([treeId]) @@index([userId]) } enum Role { OWNER EDITOR VIEWER } // 家族成员表 model FamilyMember { id String @id @default(cuid()) treeId String // 基本信息 surname String givenName String fullName String gender Gender generation Int generationName String? courtesyName String? artName String? posthumousName String? rank Int? // 日期 birthDate String? deathDate String? isLunarDate Boolean @default(false) // 地点 ancestralHome String? birthPlace String? burialPlace String? // 联系方式 phone String? telephone String? email String? address String? // 关系 fatherId String? motherId String? spouseIds String[] @default([]) childrenIds String[] @default([]) // 配偶父母信息 spouseFatherName String? spouseMotherName String? // 内容 bio String? tags String[] @default([]) avatarUrl String? avatarImageId String? photoIds String[] @default([]) // 元数据 createdAt DateTime @default(now()) updatedAt DateTime @updatedAt // 关系 tree FamilyTree @relation(fields: [treeId], references: [id], onDelete: Cascade) @@index([treeId]) @@index([fatherId]) @@index([motherId]) } enum Gender { MALE FEMALE } // 家族故事表 model FamilyStory { id String @id @default(cuid()) memberId String title String content String date String? tags String[] @default([]) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt } // 操作日志表 model ActivityLog { id String @id @default(cuid()) treeId String userId String action Action entityType EntityType entityId String? entityName String? changes Json? timestamp DateTime @default(now()) // 关系 tree FamilyTree @relation(fields: [treeId], references: [id], onDelete: Cascade) user User @relation(fields: [userId], references: [id], onDelete: Cascade) @@index([treeId]) @@index([userId]) @@index([timestamp]) } enum Action { CREATE UPDATE DELETE IMPORT EXPORT } enum EntityType { MEMBER PHOTO STORY SETTINGS } ``` ## 第五步:创建数据库表 ```bash # 创建迁移 npx prisma migrate dev --name init # 生成 Prisma Client npx prisma generate ``` ## 第六步:配置 NextAuth 创建 `lib/auth.ts`: ```typescript import { NextAuthOptions } from "next-auth" import CredentialsProvider from "next-auth/providers/credentials" import { PrismaAdapter } from "@auth/prisma-adapter" import { prisma } from "./prisma" import bcrypt from "bcryptjs" export const authOptions: NextAuthOptions = { adapter: PrismaAdapter(prisma), providers: [ CredentialsProvider({ name: "credentials", credentials: { email: { label: "Email", type: "email" }, password: { label: "Password", type: "password" } }, async authorize(credentials) { if (!credentials?.email || !credentials?.password) { throw new Error("请输入邮箱和密码") } const user = await prisma.user.findUnique({ where: { email: credentials.email } }) if (!user || !user.password) { throw new Error("用户不存在") } const isValid = await bcrypt.compare(credentials.password, user.password) if (!isValid) { throw new Error("密码错误") } return { id: user.id, email: user.email, name: user.name, } } }) ], session: { strategy: "jwt" }, pages: { signIn: "/auth/signin", }, callbacks: { async jwt({ token, user }) { if (user) { token.id = user.id } return token }, async session({ session, token }) { if (session.user) { session.user.id = token.id as string } return session } } } ``` 创建 `lib/prisma.ts`: ```typescript 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 ``` ## 第七步:创建 API Routes ### 认证 API `app/api/auth/[...nextauth]/route.ts`: ```typescript import NextAuth from "next-auth" import { authOptions } from "@/lib/auth" const handler = NextAuth(authOptions) export { handler as GET, handler as POST } ``` `app/api/auth/register/route.ts`: ```typescript import { NextRequest, NextResponse } from "next/server" import { prisma } from "@/lib/prisma" import bcrypt from "bcryptjs" import { z } from "zod" const registerSchema = z.object({ email: z.string().email(), password: z.string().min(6), name: z.string().optional(), }) export async function POST(req: NextRequest) { try { const body = await req.json() const { email, password, name } = registerSchema.parse(body) // 检查用户是否已存在 const existingUser = await prisma.user.findUnique({ where: { email } }) if (existingUser) { return NextResponse.json( { error: "用户已存在" }, { status: 400 } ) } // 加密密码 const hashedPassword = await bcrypt.hash(password, 10) // 创建用户 const user = await prisma.user.create({ data: { email, password: hashedPassword, name, } }) return NextResponse.json({ user: { id: user.id, email: user.email, name: user.name, } }) } catch (error) { console.error("注册失败:", error) return NextResponse.json( { error: "注册失败" }, { status: 500 } ) } } ``` ### 家族树 API `app/api/trees/route.ts`: ```typescript import { NextRequest, NextResponse } from "next/server" import { getServerSession } from "next-auth" import { authOptions } from "@/lib/auth" import { prisma } from "@/lib/prisma" // 获取用户的所有家族树 export async function GET(req: NextRequest) { const session = await getServerSession(authOptions) if (!session?.user?.id) { return NextResponse.json({ error: "未授权" }, { status: 401 }) } const trees = await prisma.familyTree.findMany({ where: { OR: [ { ownerId: session.user.id }, { collaborators: { some: { userId: session.user.id } } } ] }, include: { owner: { select: { id: true, name: true, email: true, } }, _count: { select: { members: true } } } }) return NextResponse.json({ trees }) } // 创建新家族树 export async function POST(req: NextRequest) { const session = await getServerSession(authOptions) if (!session?.user?.id) { return NextResponse.json({ error: "未授权" }, { status: 401 }) } const { name, description } = await req.json() const tree = await prisma.familyTree.create({ data: { name, description, ownerId: session.user.id, } }) return NextResponse.json({ tree }) } ``` ## 第八步:创建权限中间件 `lib/permissions.ts`: ```typescript import { prisma } from "./prisma" export async function checkPermission( userId: string, treeId: string, requiredRole: "OWNER" | "EDITOR" | "VIEWER" ) { // 检查是否是所有者 const tree = await prisma.familyTree.findFirst({ where: { id: treeId, ownerId: userId } }) if (tree) { return { hasPermission: true, role: "OWNER" } } // 检查协作者权限 const collaborator = await prisma.treeCollaborator.findFirst({ where: { treeId, userId } }) if (!collaborator) { return { hasPermission: false, role: null } } const roleHierarchy = { OWNER: 3, EDITOR: 2, VIEWER: 1 } const hasPermission = roleHierarchy[collaborator.role] >= roleHierarchy[requiredRole] return { hasPermission, role: collaborator.role } } ``` ## 第九步:部署到 Vercel 1. **推送代码到 GitHub** 2. **在 Vercel 创建项目** - 导入 GitHub 仓库 - 配置环境变量 3. **配置 Vercel Postgres** ```bash # 在 Vercel 项目中 Storage -> Create Database -> Postgres ``` 4. **设置环境变量** - `DATABASE_URL`(自动设置) - `NEXTAUTH_URL` - `NEXTAUTH_SECRET` 5. **部署** ```bash git push origin main ``` ## 下一步 完成后端配置后,需要: 1. 更新前端代码,使用 API 而不是 IndexedDB 2. 添加登录/注册页面 3. 实现数据迁移工具(从 IndexedDB 到 PostgreSQL) 4. 添加协作者邀请功能 --- **需要我继续实施这些步骤吗?**