目录
- 前言
- [一、 数据建模:定义系统的"基因"](#一、 数据建模:定义系统的“基因”)
-
- [💡 架构映射:低代码 vs 代码](#💡 架构映射:低代码 vs 代码)
- [二、 工程化流水线:从模型到可用数据](#二、 工程化流水线:从模型到可用数据)
-
- [2.1 配置自动化填充(Seed)](#2.1 配置自动化填充(Seed))
- [2.2 发布数据源](#2.2 发布数据源)
- [三、 核心实现:封装"低代码级别"的分页 API](#三、 核心实现:封装“低代码级别”的分页 API)
-
- [3.1 定义参数协议](#3.1 定义参数协议)
- [3.2 分页查询的底层逻辑](#3.2 分页查询的底层逻辑)
- [四、 路由封装:暴露数据服务](#四、 路由封装:暴露数据服务)
- [五、 API 的"可测试性":工程化的基石](#五、 API 的“可测试性”:工程化的基石)
-
- [5.1 浏览器测试](#5.1 浏览器测试)
- [5.2 单元测试(推荐)](#5.2 单元测试(推荐))
- 总结:从"写业务"到"写引擎"
前言
在上一章中,我们完成了系统的整体架构设计,明确了"门户归入口(app),业务归模块(modules)"的原则。但此时的系统仅仅是一个精致的"空壳"。
一个真实的业务系统,其核心生命力源于数据能力(Data Layer)。
在低代码平台中,这一步通常表现为:
可视化创建模型 → 一键生成 API → UI 组件直接绑定数据源
但在全栈开发的世界里,为了获得更高的灵活性和掌控力,我们需要亲手构建这套"数据引擎"。本章我们将复刻低代码的高效体验,在 Next.js 中实现一套生产级的数据模型与分页 API。
一、 数据建模:定义系统的"基因"
打开 prisma/schema.prisma,我们定义最基础的用户模型。虽然这只是一个单表结构,但它是所有业务逻辑的起点。
prisma
// prisma/schema.prisma
// 1. 定义枚举类型
enum UserStatus {
ACTIVE // 在职
RESIGNED // 离职
ON_LEAVE // 休假
}
// 2. 更新模型
model User {
id String @id @default(cuid())
name String
email String? @unique
phone String?
// 使用枚举作为字段类型,并设置默认值
status UserStatus @default(ACTIVE)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
💡 架构映射:低代码 vs 代码
| 步骤 | 低代码操作 | Prisma 代码实现 |
|---|---|---|
| 定义表名 | 创建"用户"实体 | model User |
| 配置字段 | 添加"姓名"、"邮箱"字段 | 定义 name, email 属性 |
| 设置约束 | 勾选"唯一索引" | 添加 @unique 修饰符 |
二、 工程化流水线:从模型到可用数据
定义好模型后,我们需要通过 Prisma 的"三部曲"将模型转化为可调用的代码。特别地,我们要配置 Seed(种子数据),这相当于低代码里的"预置演示数据"。
2.1 配置自动化填充(Seed)
首先,在项目根目录创建 prisma/seed.ts。
typescript
// prisma/seed.ts
import { PrismaClient, UserStatus } from "@/generated/prisma/client";
import { PrismaPg } from "@prisma/adapter-pg";
import "dotenv/config";
const adapter = new PrismaPg({ connectionString: process.env.DATABASE_URL! });
const prisma = new PrismaClient({ adapter });
async function main() {
console.log('正在清理旧数据并填充种子数据...');
// 预置示例数据
const users = [
{ name: "Alice", email: "alice@example.com", status: UserStatus.ACTIVE },
{ name: "Bob", email: "bob@example.com", status: UserStatus.RESIGNED },
{ name: "Charlie", email: "charlie@example.com", status: UserStatus.ON_LEAVE },
];
for (const u of users) {
await prisma.user.upsert({
where: { email: u.email },
update: {},
create: u,
});
}
console.log('✅ 数据填充完成');
}
main().finally(() => prisma.$disconnect());
接着,在 prisma.config.ts 中注册该脚本:
typescript
// prisma.config.ts
export default defineConfig({
// ... 其他配置
migrations: {
seed: `tsx prisma/seed.ts`, // 告诉 Prisma 如何运行种子脚本
},
});
2.2 发布数据源
执行以下指令,完成从建模到数据落地的闭环:
bash
# 1. 同步表结构到数据库
npx prisma migrate dev --name init_user
# 2. 生成类型安全的 TypeScript 客户端
npx prisma generate
# 3. 运行种子脚本,注入初始数据
npx prisma db seed
三、 核心实现:封装"低代码级别"的分页 API
一个成熟的后台系统,表格(Table)是绝对的主角。而驱动表格的灵魂,就是一个支持分页、搜索、排序的 API。
我们不在传统的 controller 里写逻辑,而是将其内聚在 modules/user/user.api.ts 中。
3.1 定义参数协议
typescript
// modules/user/user.api.ts
import { UserStatus } from "@/generated/prisma/client"
export type UserPageParams = {
page?: number
pageSize?: number
keyword?: string
status?: UserStatus
sortField?: string
sortOrder?: "asc" | "desc"
}
3.2 分页查询的底层逻辑
typescript
// modules/user/user.api.ts
import prisma from "@/lib/prisma"
import { Prisma } from "@/generated/prisma/client"
// 允许排序字段(防止非法注入)
const allowedSortFields = ["createdAt", "name", "email"] as const
export async function getUserPage(params: UserPageParams) {
const {
page = 1,
pageSize = 10,
keyword = "",
sortField = "createdAt",
sortOrder = "desc",
} = params
const skip = (page - 1) * pageSize
// ✅ 排序字段安全控制
const safeSortField = allowedSortFields.includes(sortField as any)
? sortField
: "createdAt"
// ✅ 类型安全 where
const where: Prisma.UserWhereInput = {
...(keyword
? {
OR: [
{ name: { contains: keyword, mode: "insensitive" } },
{ email: { contains: keyword, mode: "insensitive" } },
],
}
: {}),
...(params.status ? { status: params.status } : {}),
}
const [list, total] = await Promise.all([
prisma.user.findMany({
where,
skip,
take: pageSize,
orderBy: {
[safeSortField]: sortOrder,
},
}),
prisma.user.count({ where }),
])
return {
list,
total,
page,
pageSize,
}
}
关键认知:
- 分页 = 数据切片 (
skip/take) + 总数统计 (count)。- 使用
Promise.all能显著降低网络往返带来的延迟,这是初级开发者迈向中级的必经之路。
四、 路由封装:暴露数据服务
在 app/api/users/route.ts 中,我们只需做一个简单的"请求中转"。
typescript
// app/api/users/route.ts
import { getUserPage } from "@/modules/user/user.api"
import { NextRequest, NextResponse } from "next/server"
import { UserStatus } from "@/generated/prisma/client"
export async function GET(req: NextRequest) {
const { searchParams } = new URL(req.url)
const status = searchParams.get("status") as UserStatus | null
const data = await getUserPage({
page: Number(searchParams.get("page")) || 1,
pageSize: Number(searchParams.get("pageSize")) || 10,
keyword: searchParams.get("keyword") || "",
status: status || undefined,
})
return NextResponse.json(data)
}
五、 API 的"可测试性":工程化的基石
在低代码中,你可以点击"测试接口"即时查看 JSON。在专业开发中,我们追求的是自动化验证。
5.1 浏览器测试
bash
http://localhost:3000/api/users?page=1&pageSize=10
5.2 单元测试(推荐)
安装:
bash
npm install -D vitest
npm install -D vite-tsconfig-paths
npm install -D dotenv
创建测试文件:
typescript
// modules/user/user.api.test.ts
import { describe, it, expect } from "vitest"
import { getUserPage } from "./user.api"
describe("User API 测试", () => {
it("应该返回正确的分页结构", async () => {
const result = await getUserPage({
page: 1,
pageSize: 5,
})
expect(Array.isArray(result.list)).toBe(true)
expect(result.page).toBe(1)
expect(result.pageSize).toBe(5)
expect(result.total).toBeGreaterThanOrEqual(0)
})
})
创建配置文件:
bash
import { defineConfig } from "vitest/config"
import tsconfigPaths from "vite-tsconfig-paths"
import dotenv from "dotenv"
// 手动加载 env
dotenv.config()
export default defineConfig({
plugins: [tsconfigPaths()],
test: {
environment: "node",
},
})
在 package.json 加:
bash
{
"scripts": {
"test": "vitest",
"test:run": "vitest run"
}
}
运行测试
bash
npm run test
总结:从"写业务"到"写引擎"
很多人认为写 API 就是在写 CRUD,但本章我们做的事情本质上是:构建一个标准化的数据服务层(Data Service Layer)。
我们通过 Prisma 实现了:
- 强类型约束:模型即代码,避免字段名写错。
- 高性能查询:理解并应用了分页偏移算法与并发查询。
- 可验证性:引入测试意识,让 API 从"跑得通"变成"打不烂"。
下一章预告
数据源已经就绪,接下来我们要进入视觉呈现阶段:
👉 用户列表 UI 实现:构建高性能表格组件
我们将重点探讨:
- 表格组件化:如何利用 shadcn/ui 快速搭建 Data Table。
- 状态驱动:如何让筛选、分页与 URL 联动。
- 请求编排:前端如何优雅地消费我们刚刚写好的分页 API。
准备好,我们将赋予数据以形态!