注入灵魂:从架构设计到数据能力的“降维打击”

目录

  • 前言
  • [一、 数据建模:定义系统的"基因"](#一、 数据建模:定义系统的“基因”)
    • [💡 架构映射:低代码 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 实现了:

  1. 强类型约束:模型即代码,避免字段名写错。
  2. 高性能查询:理解并应用了分页偏移算法与并发查询。
  3. 可验证性:引入测试意识,让 API 从"跑得通"变成"打不烂"。

下一章预告

数据源已经就绪,接下来我们要进入视觉呈现阶段:

👉 用户列表 UI 实现:构建高性能表格组件

我们将重点探讨:

  • 表格组件化:如何利用 shadcn/ui 快速搭建 Data Table。
  • 状态驱动:如何让筛选、分页与 URL 联动。
  • 请求编排:前端如何优雅地消费我们刚刚写好的分页 API。

准备好,我们将赋予数据以形态!

相关推荐
胡志辉的博客12 天前
本地明明好好的,怎么一上线就跨域了?把同源策略、前后端分工和 CORS 一次讲明白
前端·javascript·vue.js·reactjs·nextjs·跨域
康一夏21 天前
Next.js 13变化有多大?
前端·react·nextjs
低代码布道师23 天前
纯代码实战:MBA培训管理系统 (十六) ——岗位管理(新增、编辑、删除)
nextjs
低代码布道师1 个月前
纯代码实战:MBA培训管理系统 (十四) ——用户管理(批量选择与批量删除)
javascript·nextjs
Zacks_xdc1 个月前
【全栈】云服务器安装 MySQL + Next.js 连接完整 Demo
服务器·javascript·mysql·阿里云·nextjs·云服务器
念念不忘 必有回响1 个月前
Drizzle ORM上手指南:在Next.js中优雅地操作PostgreSQL
开发语言·postgresql·nodejs·nextjs·drizzle
念念不忘 必有回响1 个月前
Next.js 14-16 全栈开发实战:从 App Router 核心原理到 Server Actions 深度剖析
前端·nextjs
胡西风_foxww1 个月前
nextjs部署更新,Turbopack 和 Webpack 缓存冲突问题解决
缓存·webpack·react·nextjs·turbopack
C_心欲无痕3 个月前
Next.js 的哲学思想
开发语言·前端·javascript·ecmascript·nextjs