Files
chinese-family-tree-2/后端实施步骤.md
T
freedakgmail 626d7dddde 0.0.0.5
2025-11-23 00:49:16 +08:00

580 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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. 添加协作者邀请功能
---
**需要我继续实施这些步骤吗?**