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

12 KiB
Raw Blame History

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 - 环境变量文件

第三步:配置数据库连接

选择数据库提供商

推荐选项:

  1. Vercel Postgres(推荐)

    • 与 Vercel 部署完美集成
    • 免费额度:60小时计算时间/月
    • 自动备份
  2. Neon

    • 无服务器 PostgreSQL
    • 免费额度:3GB 存储
    • 自动扩展
  3. 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

  1. 推送代码到 GitHub

  2. 在 Vercel 创建项目

    • 导入 GitHub 仓库
    • 配置环境变量
  3. 配置 Vercel Postgres

    # 在 Vercel 项目中
    Storage -> Create Database -> Postgres
    
  4. 设置环境变量

    • DATABASE_URL(自动设置)
    • NEXTAUTH_URL
    • NEXTAUTH_SECRET
  5. 部署

    git push origin main
    

下一步

完成后端配置后,需要:

  1. 更新前端代码,使用 API 而不是 IndexedDB
  2. 添加登录/注册页面
  3. 实现数据迁移工具(从 IndexedDB 到 PostgreSQL
  4. 添加协作者邀请功能

需要我继续实施这些步骤吗?