本文将带你从零搭建一个完整的单词后台管理系统,涵盖数据库设计、ORM 集成、权限认证、数据导入等全流程。
一、项目介绍
1.1 项目背景
在英语学习领域,高质量的单词数据是核心资产。本项目旨在构建一个单词后台管理系统,用于管理单词书、清洗数据、导入 GitHub 高星单词库,最终为 H5 应用提供数据支持。
1.2 应用形式
- 后台管理系统:管理员维护单词书、用户管理
- H5 应用:面向终端用户的单词学习应用
- 多端开发:一套后端,多端复用
1.3 技术栈
| 技术 | 用途 |
|---|---|
| Next.js 16 | 全栈框架 |
| Supabase | 云端 PostgreSQL 数据库 |
| Drizzle ORM | 数据库 ORM |
| shadcn/ui | UI 组件库 |
| Tailwind CSS | 样式框架 |
| Zustand | 状态管理 |
二、Supabase 云端数据库
2.1 为什么选择 Supabase?
Supabase 是一个 BaaS(Backend as a Service) 平台,提供:
- 🚀 零成本部署:免费额度足够开发和小规模生产
- 🔒 安全性:内置 Row Level Security (RLS)
- 📈 可扩展性:支持向量数据库、实时订阅
- 🗄️ PostgreSQL:完整的关系型数据库能力
2.2 创建数据库
- 访问 supabase.com 注册账号
- 创建新项目,选择区域(建议 Asia)
- 获取数据库连接字符串:
bash
postgresql://postgres:[YOUR-PASSWORD]@db.[PROJECT-REF].supabase.co:5432/postgres
- 在项目根目录创建
.env文件:
env
DATABASE_URL=postgresql://postgres:Hjf206421728@db.qiyhxysaksemfpfpwafc.supabase.co:5432/postgres
三、Drizzle ORM 集成
3.1 什么是 ORM?
ORM(Object-Relational Mapping) 对象关系映射,让我们可以用面向对象的方式操作数据库:
typescript
// 传统 SQL
INSERT INTO users (name, email) VALUES ('张三', 'zhangsan@example.com');
// ORM 写法
await db.insert(users).values({ name: '张三', email: 'zhangsan@example.com' });
3.2 安装依赖
bash
# 生产依赖
pnpm add drizzle-orm postgres bcryptjs uuid
# 开发依赖
pnpm add -D drizzle-kit @types/bcryptjs @types/uuid
3.3 项目结构
bash
lib/db/
├── index.ts # 数据库连接配置
└── schema.ts # 表结构定义(Schema)
3.4 数据库连接
lib/db/index.ts
typescript
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
typescript
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 常用命令
bash
pnpm db:generate # 生成迁移文件
pnpm db:migrate # 执行迁移
pnpm db:push # 直接推送 Schema 到数据库
pnpm db:studio # 打开可视化工具
四、数据库表设计
4.1 管理员表(admin_users)
typescript
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)
typescript
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)
typescript
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 推送表结构到数据库
bash
pnpm db:push
执行后,Supabase 会自动创建对应的表。
五、权限认证系统
5.1 认证流程
bash
首次访问 → 检查是否有管理员
↓
无管理员 → 跳转 /signup(注册系统管理员)
有管理员 → 跳转 /signin(登录)
↓
登录成功 → 创建 Session(7天有效期)→ 写入 Cookie
↓
后续请求 → 从 Cookie 读取 Session → 验证用户身份
5.2 核心工具函数
lib/auth.ts
typescript
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-run |
GET | 检查是否首次运行 | 公开 |
/api/auth/signup |
POST | 注册系统管理员 | 仅首次 |
/api/auth/signin |
POST | 登录 | 公开 |
/api/auth/signout |
POST | 退出登录 | 已登录 |
/api/auth/me |
GET | 获取当前用户 | 已登录 |
/api/admin-users |
GET | 管理员列表 | 系统管理员 |
/api/admin-users |
POST | 创建管理员 | 系统管理员 |
/api/admin-users/[id] |
PUT | 编辑管理员 | 系统管理员 |
/api/admin-users/[id] |
DELETE | 删除管理员 | 系统管理员 |
5.4 登录接口示例
app/api/auth/signin/route.ts
typescript
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
typescript
"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 侧边栏权限控制
typescript
// 根据用户角色决定显示哪些菜单
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 的提示词:
arduino
写一个 Node.js 脚本,把 JSON 格式的单词数据转成 CSV。
输入示例:[{ "word": "abandon", "definition": "v. 放弃" }]
输出格式:word,definition,phonetic,translation
要求:本地运行,处理 1000 条数据
生成的脚本约 1000 token,本地运行即可完成转换。
7.4 导入 Supabase
- 转换为 CSV 后,进入 Supabase Dashboard
- 选择 Table Editor → Import data
- 上传 CSV 文件
- 映射字段,完成导入
八、shadcn/ui 组件库
8.1 为什么选择 shadcn/ui?
- 定制性强:代码在本地,随意修改
- AI 友好:语义化类名,Tailwind CSS 配合
- 按需加载:只安装需要的组件
- 无依赖:不需要额外的 UI 库
8.2 安装组件
bash
npx shadcn@latest add button
npx shadcn@latest add card
npx shadcn@latest add input
npx shadcn@latest add label
组件会安装到 components/ui/ 目录下。
8.3 使用示例
tsx
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 提交格式
makefile
<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 项目亮点
- 零成本部署:Supabase 免费额度足够开发
- 类型安全:Drizzle ORM + TypeScript 全链路类型推导
- 权限完善:Session 认证 + 角色控制
- 数据清洗:支持从 GitHub 导入高质量单词库
- AI 友好:shadcn/ui 语义化组件,Tailwind CSS 样式
10.2 后续计划
- 单词书 CRUD 功能
- 单词数据导入(JSON → CSV → 数据库)
- H5 单词学习应用
- 向量数据库支持(语义搜索)
- 多端适配(小程序、App)
项目地址 :GitHub - danci1
技术交流:欢迎在评论区留言讨论!
📝 作者 :h206421 📅 发布时间 :2026 年 8 月 🏷️ 标签:Next.js, Supabase, Drizzle ORM, shadcn/ui, 全栈开发