从零搭建 Next.js 单词管理系统:Supabase + Drizzle ORM + shadcn/ui 全栈实战

34 阅读7分钟

本文将带你从零搭建一个完整的单词后台管理系统,涵盖数据库设计、ORM 集成、权限认证、数据导入等全流程。

一、项目介绍

1.1 项目背景

在英语学习领域,高质量的单词数据是核心资产。本项目旨在构建一个单词后台管理系统,用于管理单词书、清洗数据、导入 GitHub 高星单词库,最终为 H5 应用提供数据支持。

1.2 应用形式

  • 后台管理系统:管理员维护单词书、用户管理
  • H5 应用:面向终端用户的单词学习应用
  • 多端开发:一套后端,多端复用

1.3 技术栈

技术用途
Next.js 16全栈框架
Supabase云端 PostgreSQL 数据库
Drizzle ORM数据库 ORM
shadcn/uiUI 组件库
Tailwind CSS样式框架
Zustand状态管理

二、Supabase 云端数据库

2.1 为什么选择 Supabase?

Supabase 是一个 BaaS(Backend as a Service) 平台,提供:

  • 🚀 零成本部署:免费额度足够开发和小规模生产
  • 🔒 安全性:内置 Row Level Security (RLS)
  • 📈 可扩展性:支持向量数据库、实时订阅
  • 🗄️ PostgreSQL:完整的关系型数据库能力

2.2 创建数据库

  1. 访问 supabase.com 注册账号
  2. 创建新项目,选择区域(建议 Asia)
  3. 获取数据库连接字符串:
postgresql://postgres:[YOUR-PASSWORD]@db.[PROJECT-REF].supabase.co:5432/postgres
  1. 在项目根目录创建 .env 文件:
DATABASE_URL=postgresql://postgres:Hjf206421728@db.qiyhxysaksemfpfpwafc.supabase.co:5432/postgres

三、Drizzle ORM 集成

3.1 什么是 ORM?

ORM(Object-Relational Mapping) 对象关系映射,让我们可以用面向对象的方式操作数据库:

// 传统 SQL
INSERT INTO users (name, email) VALUES ('张三', 'zhangsan@example.com');

// ORM 写法
await db.insert(users).values({ name: '张三', email: 'zhangsan@example.com' });

3.2 安装依赖

# 生产依赖
pnpm add drizzle-orm postgres bcryptjs uuid

# 开发依赖
pnpm add -D drizzle-kit @types/bcryptjs @types/uuid

3.3 项目结构

lib/db/
├── index.ts      # 数据库连接配置
└── schema.ts     # 表结构定义(Schema)

3.4 数据库连接

lib/db/index.ts

import { drizzle } from "drizzle-orm/postgres-js";
import postgres from "postgres";
import * as schema from "./schema";

const connectionString = process.env.DATABASE_URL!;

const client = postgres(connectionString, {
  ssl: "require",
});

export const db = drizzle(client, { schema });

3.5 Drizzle 配置

drizzle.config.ts

import { defineConfig } from "drizzle-kit";

export default defineConfig({
  schema: "./lib/db/schema.ts",
  out: "./drizzle",
  dialect: "postgresql",
  dbCredentials: {
    url: process.env.DATABASE_URL!,
  },
});

3.6 常用命令

pnpm db:generate   # 生成迁移文件
pnpm db:migrate    # 执行迁移
pnpm db:push       # 直接推送 Schema 到数据库
pnpm db:studio     # 打开可视化工具

四、数据库表设计

4.1 管理员表(admin_users)

import { pgTable, serial, text, timestamp } from "drizzle-orm/pg-core";

export const adminUsers = pgTable("admin_users", {
  id: serial("id").primaryKey(),
  name: text("name").notNull(),
  email: text("email").notNull().unique(),
  password: text("password").notNull(),  // bcrypt 加密
  role: text("role", { enum: ["system_admin", "admin"] })
    .notNull()
    .default("admin"),
  createdAt: timestamp("created_at").defaultNow().notNull(),
});

角色说明:

  • system_admin:系统管理员,拥有所有权限
  • admin:普通管理员,仅能管理单词书

4.2 会话表(admin_sessions)

export const adminSessions = pgTable("admin_sessions", {
  id: text("id").primaryKey(),  // UUID
  userId: integer("user_id")
    .notNull()
    .references(() => adminUsers.id, { onDelete: "cascade" }),
  expiresAt: timestamp("expires_at").notNull(),  // 7天有效期
  createdAt: timestamp("created_at").defaultNow().notNull(),
});

4.3 单词表(words)

export const words = pgTable("words", {
  id: serial("id").primaryKey(),
  word: text("word").notNull(),
  phonetic: text("phonetic"),  // 音标
  definition: text("definition").notNull(),
  translation: text("translation"),
  examples: text("examples"),  // JSON 字符串
  bookId: integer("book_id").references(() => books.id),
  createdAt: timestamp("created_at").defaultNow().notNull(),
});

4.4 推送表结构到数据库

pnpm db:push

执行后,Supabase 会自动创建对应的表。


五、权限认证系统

5.1 认证流程

首次访问 → 检查是否有管理员
    ↓
无管理员 → 跳转 /signup(注册系统管理员)
有管理员 → 跳转 /signin(登录)
    ↓
登录成功 → 创建 Session(7天有效期)→ 写入 Cookie
    ↓
后续请求 → 从 Cookie 读取 Session → 验证用户身份

5.2 核心工具函数

lib/auth.ts

import bcrypt from "bcryptjs";
import { v4 as uuidv4 } from "uuid";
import { cookies } from "next/headers";

const SESSION_DURATION = 7 * 24 * 60 * 60 * 1000; // 7 天

// 密码加密
export async function hashPassword(password: string): Promise<string> {
  return bcrypt.hash(password, 10);
}

// 密码验证
export async function verifyPassword(password: string, hashedPassword: string) {
  return bcrypt.compare(password, hashedPassword);
}

// 检查是否首次运行
export async function isFirstRun(): Promise<boolean> {
  const result = await db.select({ value: count() }).from(adminUsers);
  return result[0].value === 0;
}

// 创建 Session
export async function createSession(userId: number): Promise<string> {
  const sessionId = uuidv4();
  const expiresAt = new Date(Date.now() + SESSION_DURATION);

  await db.insert(adminSessions).values({
    id: sessionId,
    userId,
    expiresAt,
  });

  return sessionId;
}

// 获取当前用户
export async function getCurrentUser() {
  const cookieStore = await cookies();
  const sessionId = cookieStore.get("admin_session")?.value;

  if (!sessionId) return null;

  // 查询 Session 并验证有效期
  const session = await db.select().from(adminSessions)
    .where(eq(adminSessions.id, sessionId)).limit(1);

  if (session.length === 0 || new Date() > session[0].expiresAt) {
    return null;
  }

  // 查询用户信息
  const user = await db.select().from(adminUsers)
    .where(eq(adminUsers.id, session[0].userId)).limit(1);

  return user[0] || null;
}

5.3 API 路由设计

路由方法说明权限
/api/auth/check-first-runGET检查是否首次运行公开
/api/auth/signupPOST注册系统管理员仅首次
/api/auth/signinPOST登录公开
/api/auth/signoutPOST退出登录已登录
/api/auth/meGET获取当前用户已登录
/api/admin-usersGET管理员列表系统管理员
/api/admin-usersPOST创建管理员系统管理员
/api/admin-users/[id]PUT编辑管理员系统管理员
/api/admin-users/[id]DELETE删除管理员系统管理员

5.4 登录接口示例

app/api/auth/signin/route.ts

import { NextRequest, NextResponse } from "next/server";
import { db } from "@/lib/db";
import { adminUsers } from "@/lib/db/schema";
import { eq } from "drizzle-orm";
import { verifyPassword, createSession, setSessionCookie } from "@/lib/auth";

export async function POST(request: NextRequest) {
  const { email, password } = await request.json();

  // 查找用户
  const users = await db.select().from(adminUsers)
    .where(eq(adminUsers.email, email)).limit(1);

  if (users.length === 0) {
    return NextResponse.json({ error: "邮箱或密码错误" }, { status: 401 });
  }

  const user = users[0];

  // 验证密码
  const isValid = await verifyPassword(password, user.password);
  if (!isValid) {
    return NextResponse.json({ error: "邮箱或密码错误" }, { status: 401 });
  }

  // 创建 Session
  const sessionId = await createSession(user.id);

  const response = NextResponse.json({
    message: "登录成功",
    user: { id: user.id, name: user.name, email: user.email, role: user.role },
  });

  response.cookies.set(setSessionCookie(sessionId));
  return response;
}

六、前端页面实现

6.1 首页跳转逻辑

app/page.tsx

"use client"

import { useEffect } from "react"
import { useRouter } from "next/navigation"

export default function HomePage() {
  const router = useRouter()

  useEffect(() => {
    async function checkAuth() {
      // 检查是否首次运行
      const res = await fetch("/api/auth/check-first-run")
      const data = await res.json()

      if (data.isFirstRun) {
        router.push("/signup")  // 无管理员 → 注册
        return
      }

      // 检查是否已登录
      const meRes = await fetch("/api/auth/me")
      if (meRes.ok) {
        router.push("/books")   // 已登录 → 单词书
      } else {
        router.push("/signin")  // 未登录 → 登录
      }
    }
    checkAuth()
  }, [router])

  return <div>加载中...</div>
}

6.2 管理员管理页面

核心功能:

  • 管理员列表展示
  • 新增管理员(设置姓名、邮箱、密码、角色)
  • 编辑管理员(系统管理员不能修改自己的角色)
  • 删除管理员(不能删除自己)

6.3 侧边栏权限控制

// 根据用户角色决定显示哪些菜单
const navItems = [
  { title: "单词书", href: "/books", icon: BookOpen },
  // 仅系统管理员可见
  ...(user?.role === "system_admin"
    ? [{ title: "管理员", href: "/admin-users", icon: Users }]
    : []),
]

七、数据导入:JSON → 数据库

7.1 场景

从 GitHub 下载单词库(JSON 格式,约 178KB),需要导入 Supabase 数据库。

7.2 方案对比

方案优点缺点
AI 上下文直接转简单Token 消耗大(178KB)
AI 写转换脚本Token 少(~1000)需要本地运行
数据库直接导入最快需要格式匹配

7.3 推荐方案:AI 写转换脚本

给 AI 的提示词:

写一个 Node.js 脚本,把 JSON 格式的单词数据转成 CSV。
输入示例:[{ "word": "abandon", "definition": "v. 放弃" }]
输出格式:word,definition,phonetic,translation
要求:本地运行,处理 1000 条数据

生成的脚本约 1000 token,本地运行即可完成转换。

7.4 导入 Supabase

  1. 转换为 CSV 后,进入 Supabase Dashboard
  2. 选择 Table Editor → Import data
  3. 上传 CSV 文件
  4. 映射字段,完成导入

八、shadcn/ui 组件库

8.1 为什么选择 shadcn/ui?

  • 定制性强:代码在本地,随意修改
  • AI 友好:语义化类名,Tailwind CSS 配合
  • 按需加载:只安装需要的组件
  • 无依赖:不需要额外的 UI 库

8.2 安装组件

npx shadcn@latest add button
npx shadcn@latest add card
npx shadcn@latest add input
npx shadcn@latest add label

组件会安装到 components/ui/ 目录下。

8.3 使用示例

import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"

export default function LoginForm() {
  return (
    <Card className="w-full max-w-md">
      <CardHeader>
        <CardTitle>登录</CardTitle>
      </CardHeader>
      <CardContent className="space-y-4">
        <div className="space-y-2">
          <Label htmlFor="email">邮箱</Label>
          <Input id="email" type="email" placeholder="请输入邮箱" />
        </div>
        <Button className="w-full">登录</Button>
      </CardContent>
    </Card>
  )
}

九、Git 规范:Conventional Commits

9.1 提交格式

<type>(<scope>): <description>

feat(auth): 添加管理员登录功能
fix(api): 修复权限验证问题
docs(readme): 更新项目文档
refactor(db): 优化数据库查询
style(ui): 调整按钮样式
test(auth): 添加登录测试用例
chore(deps): 升级依赖版本

9.2 常用类型

类型说明
feat新增功能
fix修复 bug
docs文档变更
refactor代码重构
style样式变更
test测试变更
chore构建工具变更

十、总结

10.1 项目亮点

  1. 零成本部署:Supabase 免费额度足够开发
  2. 类型安全:Drizzle ORM + TypeScript 全链路类型推导
  3. 权限完善:Session 认证 + 角色控制
  4. 数据清洗:支持从 GitHub 导入高质量单词库
  5. AI 友好:shadcn/ui 语义化组件,Tailwind CSS 样式

10.2 后续计划

  • 单词书 CRUD 功能
  • 单词数据导入(JSON → CSV → 数据库)
  • H5 单词学习应用
  • 向量数据库支持(语义搜索)
  • 多端适配(小程序、App)

项目地址GitHub - danci1

技术交流:欢迎在评论区留言讨论!


📝 作者:h206421 📅 发布时间:2026 年 8 月 🏷️ 标签:Next.js, Supabase, Drizzle ORM, shadcn/ui, 全栈开发