外观
分层最佳实践
本章以 User(用户管理) 为例,提供完整的 Controller → Service → Prisma 分层开发规范,覆盖 Zod 验证、软删除、审计字段的全链路。
分层职责
| 层 | 职责 | 不做什么 |
|---|---|---|
| Controller | 接收参数 + Zod() 验证 | 不处理业务逻辑 |
| Service | 业务编排 + 数据访问 | 不操作 Prisma 时间戳 |
| Prisma | @default(now()) + @updatedAt | 时间戳由数据库/扩展管理 |
| Extensions | 软删除 + 审计字段自动填充 | 业务层无需手写 |
Service 直接操作 Prisma
Service 直接注入 PrismaService,使用 this.prisma.xxx 配合工具函数(prismaPage、buildPrismaQuery、prismaSoftRemove 等)完成数据访问。 事务场景使用 @PrismaTransactional() + usePrismaClient(),天然支持跨类传播。
Repository 层(PrismaRepository)为可选层,适合需要统一封装复用查询逻辑的场景。
1. Prisma Schema 定义
prisma
enum Role {
ADMIN
USER
GUEST
}
model User {
id String @id @default(cuid())
username String @unique @db.VarChar(50)
password String @db.VarChar(255)
phone String? @db.VarChar(20)
nickname String? @db.VarChar(50)
avatar String? @db.VarChar(500)
role Role @default(USER)
status Int @default(1) @db.TinyInt // 0=禁用 1=启用
// 审计字段(由中间件自动管理)
createdAt DateTime @default(now()) @map("created_at")
createdBy String? @map("created_by")
updatedAt DateTime @updatedAt @map("updated_at")
updatedBy String? @map("updated_by")
deletedAt DateTime? @map("deleted_at")
@@map("sys_user")
}注意: createdAt、updatedAt 由 Prisma 自动管理,createdBy、updatedBy 由审计中间件填充,deletedAt 由软删除中间件处理。
2. 模块注册(声明式扩展)
AuthModule 内置了基于 nestjs-cls(ClsModule)的请求上下文管理。AuthModule.register() 自动注册 ClsModule.forRoot({ global: true, middleware: { mount: true } }),在请求进入时由 ClsMiddleware 创建 CLS 上下文,RequestContextInterceptor 在 Guard 之后将 request.user 写入 CLS 存储。业务代码通过 getCurrentUserId() / getCurrentUser() 即可获取当前用户,无需手动注册 ClsModule。
typescript
// app.module.ts
import { Module } from '@nestjs/common'
import { PrismaModule, AuthModule, getCurrentUserId } from '@maxtan/nest-core'
import { PrismaService } from './prisma/prisma.service'
@Module({
imports: [
// AuthModule 内置 ClsModule(nestjs-cls),自动创建请求 CLS 上下文
// ClsMiddleware 创建上下文 → Guard 解析 JWT → 拦截器将 request.user 写入 CLS
AuthModule.register({ secret: process.env.JWT_SECRET! }),
PrismaModule.forRoot({
service: PrismaService,
middlewares: {
softDelete: {
enabled: true,
models: 'auto' // 自动发现含 deletedAt 的模型
},
audit: {
enabled: true,
getUserId: getCurrentUserId, // 直接从 core 导入,零配置
fallbackUserId: 'system' // 匿名/定时任务回退值
}
}
})
]
})
export class AppModule {}执行时序:
请求 → ClsMiddleware (CLS 上下文创建) → AuthGuard (JWT 验证)
↓
RequestContextInterceptor
(自动将 request.user 写入 CLS 存储)
↓
Pipes → Controller → Service → Prisma Extensions
├── 审计扩展调用 getCurrentUserId()
└── 软删除扩展自动追加条件为什么用拦截器而不是中间件写入用户? NestJS 中间件在 Guard 之前执行,此时
request.user尚未被 AuthGuard 填充。CLS 上下文由ClsMiddleware创建,但用户数据由全局拦截器在 Guard 之后写入。
非鉴权场景的自动降级:
| 场景 | request.user | getCurrentUserId() | 审计字段 |
|---|---|---|---|
| 已登录用户 | AuthPayload | 'user-id-xxx' | 填充实际用户 ID |
@AuthPublic() 公开路由 | undefined | null | 使用 fallbackUserId(如 'system') |
| 定时任务 / 队列消费 | 无请求上下文 | null | 使用 fallbackUserId |
3. Zod Schema 定义
typescript
// user.schema.ts
import { z } from 'zod'
import {
zId, zIds, zStr, zStrReq, zStrNull, zVarChar, zVarCharNull,
zEnumInt, zEnumStr, zQueryEnumInt, zQueryEnumStr, zPageWithKeyword, zDateRange
} from '@maxtan/nest-core'
// ─── 枚举常量(单一来源,Schema + 业务逻辑共享) ───
/** 用户状态:0-禁用 1-启用(对应 @db.TinyInt) */
export enum UserStatus {
DISABLED = 0,
ENABLED = 1
}
/** 用户角色值(对应 Prisma enum Role) */
const ROLE_VALUES = ['ADMIN', 'USER', 'GUEST'] as const
const role = zEnumStr(...ROLE_VALUES)
// ─── Schema ───
// ═══ 创建用户 ═══
export const CreateUser = z.object({
username: zVarChar(50, 2), // @db.VarChar(50),最少 2 字符
password: z.string().min(6).max(255), // @db.VarChar(255),最少 6 字符
nickname: zVarCharNull(50), // 可空列,空串表示清空 -> null
phone: zVarCharNull(20), // 同上
avatar: zVarChar(500).optional(), // 可空列 → .optional()
intro: zStrNull, // Text? / Json 内部普通可空文本
role: role.optional(), // 有默认值 → .optional()
status: zEnumInt(0, 1).optional() // TinyInt 枚举,有默认值 → .optional()
})
export type CreateUser = z.infer<typeof CreateUser>
// ═══ 更新用户 ═══
export const UpdateUser = CreateUser.partial().extend({
id: zId // 更新必须指定用户 ID
})
export type UpdateUser = z.infer<typeof UpdateUser>
// ═══ 列表查询 ═══
export const ListUser = zPageWithKeyword().extend({
status: zQueryEnumInt(0, 1),
role: zQueryEnumStr(...ROLE_VALUES)
}).merge(zDateRange()) // 时间范围
export type ListUser = z.infer<typeof ListUser>
// ═══ 批量删除 ═══
export const DeleteUsers = z.object({ ids: zIds })
export type DeleteUsers = z.infer<typeof DeleteUsers>原则:
- 字符串优先
zVarChar,长度对齐 Prisma@db.VarChar(N) - 普通可选文本优先
zStr,可空文本优先zStrNull - 整型枚举用
zEnumInt,字符串枚举用zEnumStr - ID 用
zId/zIds - 查询过滤优先
zQueryEnumInt/zQueryEnumStr - 列表用
zPageWithKeyword().extend(...)+zDateRange() - 枚举常量集中声明,Schema + Service 共享
字符串 helper 的实践边界:
zStr:空串视为未传,适合 keyword、标题、备注等普通可选字段zStrNull/zVarCharNull:空串视为清空,适合允许NULL的数据库字段zStrReq:trim 后必须有值,适合必填文本zStrArr({ min: 1 }):字符串数组逐项 trim,适合标签、ID 列表
查询过滤优先使用 query helper 的原因:
- 前端下拉框、选择器常把初始值设为
null或'' - 列表查询的真实语义通常是“不传就不过滤”,而不是“空值非法”
- 把这层语义放进 Schema,可以避免在 Service 里手动写
status || undefined之类兼容代码 - 同时还能保留对
true、{}、ROOT这类真正非法值的报错能力
4. Service 层(业务编排 + 数据访问)
Service 直接注入 PrismaService,使用工具函数完成数据访问,天然支持事务。
typescript
// user.service.ts
import { Injectable } from '@nestjs/common'
import {
AppException, MSG, PrismaTransactional,
prismaPage, prismaKeyword, prismaSoftRemove, prismaExists
} from '@maxtan/nest-core'
import { PrismaService } from '../prisma/prisma.service'
import type {
CreateUser, UpdateUser,
ListUser, DeleteUsers
} from './user.schema'
@Injectable()
export class UserService {
constructor(private readonly prisma: PrismaService) {}
/** 创建用户 */
async create(dto: CreateUser) {
// 业务判断:用户名唯一性校验
const exists = await this.prisma.user.findUnique({
where: { username: dto.username }
})
if (exists) throw new AppException('用户名已存在')
return this.prisma.user.create({ data: dto })
// createdBy / updatedBy 由审计中间件自动填充
// createdAt 由 @default(now()) 自动填充
}
/** 更新用户 */
async update(dto: UpdateUser) {
const { id, ...data } = dto
const user = await this.prisma.user.findFirst({ where: { id } })
if (!user) throw new AppException(MSG.NOT_FOUND_DATA)
return this.prisma.user.update({ where: { id }, data })
// updatedBy 由审计中间件自动填充
// updatedAt 由 @updatedAt 自动填充
}
/** 分页查询 */
async list(dto: ListUser) {
const { current, size, keyword, status, role } = dto
return prismaPage(this.prisma.user, {
where: {
...prismaKeyword(keyword, ['username', 'nickname']),
...(status !== undefined && { status }),
...(role !== undefined && { role }),
},
current,
size,
orderBy: { createdAt: 'desc' },
select: {
id: true, username: true, nickname: true,
phone: true, avatar: true, role: true,
status: true, createdAt: true
}
})
}
/** 详情 */
async detail(id: string) {
const record = await this.prisma.user.findFirst({ where: { id } })
if (!record) throw new AppException(MSG.NOT_FOUND_DATA)
return record
}
/** 批量删除 */
async remove(dto: DeleteUsers) {
return prismaSoftRemove(this.prisma.user, dto.ids!)
// 软删除由 prismaSoftRemove 处理(update deletedAt)
}
/** 事务示例:创建用户并初始化配置 */
@PrismaTransactional()
async createWithProfile(dto: CreateUser) {
// this.prisma 在事务内自动返回 tx,无需手动 usePrismaClient
const user = await this.prisma.user.create({ data: dto })
await this.prisma.profile.create({ data: { userId: user.id } })
return user
}
}原则:
- Service 直接注入
PrismaService,使用this.prisma.xxx操作数据 - 分页用
prismaPage(),模糊搜索用prismaKeyword(),软删除用prismaSoftRemove() - 事务用
@PrismaTransactional()+usePrismaClient() - 不手写
createdAt / updatedAt / createdBy / updatedBy - 业务判断(如唯一性校验)在 Service 层
5. Controller 层(参数接收 + 验证)
typescript
// user.controller.ts
import { Controller, Get, Post, Body, Param, Query } from '@nestjs/common'
import { Zod } from '@maxtan/nest-core'
import { UserService } from './user.service'
import {
CreateUser, UpdateUser, ListUser, DeleteUsers,
} from './user.schema'
@Controller('user')
export class UserController {
constructor(private readonly service: UserService) {}
@Post('create')
create(@Body(Zod(CreateUser)) dto: CreateUser) {
return this.service.create(dto)
}
@Post('update')
update(@Body(Zod(UpdateUser)) dto: UpdateUser) {
return this.service.update(dto)
}
@Get('list')
list(@Query(Zod(ListUser)) dto: ListUser) {
return this.service.list(dto)
}
@Get('detail/:id')
detail(@Param('id') id: string) {
return this.service.detail(id)
}
@Post('delete')
remove(@Body(Zod(DeleteUsers)) dto: DeleteUsers) {
return this.service.remove(dto)
}
}原则:
- 参数级
Zod(schema)验证(推荐方式) - Controller 只做接收 + 转发
6. Repository 层(可选,复用查询逻辑)
简单 CRUD 在 Service 中直接操作 prisma.client 即可。当多个 Service 需要共享相同的查询逻辑(如复杂列表筛选)时,使用 Repository:
typescript
// user.repository.ts
import { Injectable } from '@nestjs/common'
import { PrismaRepository, buildPrismaQuery, prismaKeyword } from '@maxtan/nest-core'
import { PrismaService } from '../prisma/prisma.service'
import type { User } from '@prisma/client'
import type { ListUser } from './user.schema'
@Injectable()
export class UserRepository extends PrismaRepository<User> {
constructor(private prisma: PrismaService) {
// 传入 PrismaService 和模型名,Repository 自动感知事务上下文
super(prisma, 'user')
// softDelete: false (默认,由中间件处理)
// defaultOrderBy: { createdAt: 'desc' } (默认)
}
/**
* 复杂列表查询(使用增强版 buildPrismaQuery)
*/
async findPageByDto(dto: ListUser) {
const { keyword, current, size, ...rest } = dto
const query = buildPrismaQuery({
where: rest,
containsFields: [],
inFields: [],
dateRangeFields: [
{ start: 'startAt', end: 'endAt', target: 'createdAt' }
],
ignoreFields: [],
defaultWhere: {
...prismaKeyword(keyword, ['username', 'nickname'])
},
softDelete: false,
current,
size,
sortBy: 'createdAt',
sortOrder: 'desc',
select: {
id: true, username: true, nickname: true,
phone: true, avatar: true, role: true,
status: true, createdAt: true
}
})
const [records, total] = await Promise.all([
this.model.findMany(query),
this.model.count({ where: query.where })
])
return {
records,
total,
current: current ?? 1,
size: size ?? 20,
pages: Math.ceil(total / (size ?? 20))
}
}
}注意:
- Repository 构造使用
super(prisma, 'user')传入 PrismaService + 模型名(小写) - Repository 内部通过
usePrismaClient动态获取 model,天然支持@PrismaTransactional事务 - 软删除由中间件统一处理,Repository 的
softDelete默认为false
7. Prisma → Zod 辅助工具
在开发过程中,可以使用辅助工具快速生成 Zod schema 建议:
typescript
import { getModelFields, generateZodSuggestions, listPrismaModels } from '@maxtan/nest-core'
// 列出所有模型
const models = listPrismaModels(prisma)
// => ['User', 'Post', ...]
// 获取字段信息
const fields = getModelFields(prisma, 'User')
fields.forEach(f => {
console.log(`${f.name}: ${f.zodSuggestion} // ${f.type} ${f.dbType ?? ''}`)
})
// 生成完整建议代码
console.log(generateZodSuggestions(prisma, 'User'))输出示例:
// ═══ User Zod Schema Suggestions ═══
// export const CreateUser = z.object({
// username: zVarChar(50), // String @db.VarChar(50)
// password: zVarChar(255), // String @db.VarChar(255)
// phone: zVarChar(20).optional(), // String? @db.VarChar(20)
// nickname: zVarChar(50).optional(), // String? @db.VarChar(50)
// avatar: zVarChar(500).optional(), // String? @db.VarChar(500)
// role: zEnumStr(/* Role */).optional(), // Role @default(USER)
// status: zEnumInt(/* values */).optional(), // Int @db.TinyInt
// })完整数据流总结
请求 → ClsMiddleware (CLS 上下文创建)
↓
AuthGuard (JWT 验证) → RequestContextInterceptor (user → CLS)
↓
Controller (Zod 验证)
↓
Service (业务编排 + 数据访问)
├── prisma.client.xxx (直接操作,自动感知事务上下文)
├── prismaPage / prismaKeyword / buildPrismaQuery (工具函数)
├── prismaSoftRemove / prismaExists (CRUD 工具)
└── @PrismaTransactional (声明式事务,自动跨类传播)
↓
Prisma Extensions
├── 软删除扩展:查询自动追加 deletedAt: null
├── 审计扩展:getCurrentUserId() 从 CLS 读取 → 填充 createdBy / updatedBy
└── Prisma Engine
├── createdAt: @default(now())
└── updatedAt: @updatedAt关键原则:
- Zod 约束对齐 Prisma:
@db.VarChar(50)→zVarChar(50) - 时间戳零手写:全部由 Prisma + 扩展管理
- Service 直接操作:不需要 DAO 层,直接用
this.prisma+ 工具函数 - 事务天然集成:
this.prisma自动感知事务上下文,@PrismaTransactional跨类自动传播 - 扩展声明式注册:
PrismaModule.forRoot({ middlewares: {...} }) - 软删除自动发现:
models: 'auto'从 DMMF 读取 - 敏感字段保护:列表查询通过
select排除password等字段 - Repository 可选:需要跨 Service 复用查询逻辑时才引入
PrismaRepository - 请求上下文:基于
nestjs-cls(CLS),getCurrentUserId()在任意异步代码中可用
相关文档
- 验证管道 (Zod) — 预处理器详细 API
- Prisma 模块 — PrismaRepository、buildPrismaQuery、中间件配置
- 事务管理 —
@PrismaTransactional声明式事务 - 异常过滤器 — AppException 与业务错误码
- 响应转换 — 统一响应格式与
resMessage()