12 KiB
12 KiB
Next.js API Routes + PostgreSQL 实施步骤
第一步:安装依赖
# 数据库和 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
npx prisma init
这会创建:
prisma/schema.prisma- 数据库模型定义.env- 环境变量文件
第三步:配置数据库连接
选择数据库提供商
推荐选项:
-
Vercel Postgres(推荐)
- 与 Vercel 部署完美集成
- 免费额度:60小时计算时间/月
- 自动备份
-
Neon
- 无服务器 PostgreSQL
- 免费额度:3GB 存储
- 自动扩展
-
Supabase(仅用数据库)
- 免费额度:500MB 数据库
- 自动备份
配置 .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:
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
}
第五步:创建数据库表
# 创建迁移
npx prisma migrate dev --name init
# 生成 Prisma Client
npx prisma generate
第六步:配置 NextAuth
创建 lib/auth.ts:
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:
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:
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:
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:
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:
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
-
推送代码到 GitHub
-
在 Vercel 创建项目
- 导入 GitHub 仓库
- 配置环境变量
-
配置 Vercel Postgres
# 在 Vercel 项目中 Storage -> Create Database -> Postgres -
设置环境变量
DATABASE_URL(自动设置)NEXTAUTH_URLNEXTAUTH_SECRET
-
部署
git push origin main
下一步
完成后端配置后,需要:
- 更新前端代码,使用 API 而不是 IndexedDB
- 添加登录/注册页面
- 实现数据迁移工具(从 IndexedDB 到 PostgreSQL)
- 添加协作者邀请功能
需要我继续实施这些步骤吗?