外观
FAQ / 常见问题
安装 & 配置
Prisma 密码含特殊字符连接失败
DATABASE_URL 中如果密码包含 #、@、% 等特殊字符,需要进行 URL 编码:
| 字符 | 编码 |
|---|---|
# | %23 |
@ | %40 |
% | %25 |
: | %3A |
bash
# 错误 ❌
DATABASE_URL="mysql://root:pass#123@localhost:3306/db"
# 正确 ✅
DATABASE_URL="mysql://root:pass%23123@localhost:3306/db"PowerShell 执行 pnpm 报错 "cannot be loaded because running scripts is disabled"
PowerShell 默认限制脚本执行策略,有两种解决方案:
powershell
# 方案 1:临时修改策略(当前用户)
Set-ExecutionPolicy RemoteSigned -Scope CurrentUser
# 方案 2:使用 cmd 代替
cmd /c "pnpm run dev"如何同时使用多个数据源?
每个数据源需要一个独立的 PrismaService 子类:
typescript
// 主库
@Injectable()
class MainPrismaService extends PrismaBaseService<PrismaClient> { ... }
// 从库
@Injectable()
class ReadPrismaService extends PrismaBaseService<PrismaClient> { ... }
@Module({
imports: [
PrismaModule.forRoot({ service: MainPrismaService, token: 'MAIN_DB' }),
PrismaModule.forRoot({ service: ReadPrismaService, token: 'READ_DB', global: false }),
],
})
export class AppModule {}每个 Service 独立持有扩展配置,互不干扰。详见 Prisma 多数据源。
多数据源时 @PrismaTransactional 报错找不到 PrismaClient?
默认 prismaField 为 'prisma',多数据源时属性名通常不同(如 primaryDb),必须显式指定:
typescript
// ❌ 默认找 this.prisma,找不到
@PrismaTransactional()
async create() { ... }
// ✅ 指定正确的属性名
@PrismaTransactional({ prismaField: 'primaryDb' })
async create() { ... }多数据源时能否在一个事务里操作两个库?
不能。@PrismaTransactional 只支持单个 PrismaClient 实例的交互式事务,不支持跨数据源的分布式事务。跨库操作需要:
- 方案 1:分开事务 — 主要操作用事务保证原子性,次要操作(如日志)允许降级
- 方案 2:Saga 补偿模式 — 每步操作记录状态,失败时执行逆向补偿
- 方案 3:最终一致性 — 通过消息队列异步同步
详见 多数据源事务。
事务内调用 $transaction() 报错?
这是框架的安全防护机制。在 @PrismaTransactional 事务内调用 $transaction() 会创建独立事务, 导致原子性丢失、死锁风险和连接池耗尽。正确做法:
typescript
// ❌ 事务内禁止
const [records, total] = await this.prisma.$transaction([
this.prisma.user.findMany({ where }),
this.prisma.user.count({ where })
])
// ✅ 改用 Promise.all
const [records, total] = await Promise.all([
this.prisma.user.findMany({ where }),
this.prisma.user.count({ where })
])事务超时了怎么办?
默认超时 30s(timeout: 30000),如果业务逻辑复杂可适当调大:
typescript
@PrismaTransactional({ timeout: 60000 }) // 60 秒
async complexOperation() { ... }但更推荐拆分大事务,保持事务尽可能小:
- 不要在事务中做 HTTP 调用或重计算
- 批量操作考虑分批处理
- 只把必须保证原子性的操作放在事务内
Zod 验证
前端传字符串 "1",后端收到的类型是什么?
框架的 Zod 预处理器(如 zInt、zNum、zBool)会自动将 query-string 中的字符串转为对应类型:
typescript
// 请求: GET /users?status=1&active=true&page=2
// 经过 zInt / zBool 预处理后:
// status → number 1
// active → boolean true
// page → number 2
如果前端传 `active=''` 或 `active=null`,`zBool` 会把它视为未传,输出 `undefined`;只有显式 `false` / `0` / `no` 才会转成 `false`。zVarChar、zStr 和字符串 helper 怎么选?
| 预设 | 必填 | 长度约束 | 典型场景 |
|---|---|---|---|
zStr | 选填 | 无 | 搜索关键词、普通可选文本 |
zStr.pipe(z.string()) | 必填 | 无 | 无长度限制的必填字段 |
zStrNull | 可空 | 无 | 前端空串表示“清空为 null” |
zVarChar(n) | 必填 | max=n | 对齐 @db.VarChar(n) |
zVarCharNull(n) | 可空 | max=n | 对齐 String? @db.VarChar(n) 且空串→null |
快速判断:
- 空串应该视为“没传”:用
zStr - 空串应该视为“清空为 null”:用
zStrNull或zVarCharNull - 字段要对齐 Prisma
@db.VarChar(n):优先zVarChar/zVarCharNull - 不想再让业务代码里出现
z.string().nullable().optional():直接换成新的 nullable helper
如何自定义 Zod 验证错误消息?
框架内置 formatIssue 会自动翻译为中文,但你也可以在 schema 中指定:
typescript
const username = zVarChar(50, 2)
// 自带: "最多 50 个字符" / "至少 2 个字符"
// 自定义消息
const email = zStr.pipe(
z.string().email({ message: '请输入有效的邮箱地址' })
)为什么返回的校验错误看起来只有字段名,没有业务提示?
默认情况下,错误前缀来自字段路径,例如 username、profile.nickname,而错误正文来自两部分:
- Zod 内置规则的默认翻译,例如
最少2个字符、邮箱格式不正确 - 你在规则里显式传入的
message
如果你没有写自定义 message,也没有给字段 schema 添加 describe(),最终看到的通常就是:
json
{
"message": "username: 最少2个字符"
}如果希望提示更像业务文案,推荐分两层处理:
- 用
describe()提供字段中文名
typescript
const CreateUserSchema = z.object({
username: zStr.pipe(z.string().min(2)).describe('用户名'),
email: zStr.pipe(z.string().email()).describe('邮箱')
})这样会优先显示:
json
{
"message": "用户名: 最少2个字符"
}- 对关键业务规则显式写
message
typescript
const CreateUserSchema = z.object({
username: zStr.pipe(z.string().min(2, '用户名长度不能少于 2 位')).describe('用户名'),
email: zStr.pipe(z.string().email('请输入正确的邮箱地址')).describe('邮箱')
})这样会得到更稳定的业务提示:
json
{
"message": "用户名: 用户名长度不能少于 2 位"
}优先级规则:
- 规则上的
message优先级最高 - 没有
message时,优先使用describe()作为字段标签 - 仍取不到时,回退为原始字段路径
适用范围:
- 普通对象字段、嵌套对象字段
- 数组元素字段
pipe、preprocess、transform、optional、nullable等包装层lazy递归 schemaunion/discriminatedUnion中标签唯一的字段路径- 库内公开 helper 与组合器,如
zVarCharNull、zQueryEnumInt、zPageWithKeyword、zDateRange
如果是联合类型,还要注意一条规则:
- 同一路径在多个分支下如果对应不同的
describe()标签,框架会回退为原始 path - 这是有意为之,避免把“另一个分支”的字段名称误显示到当前错误上
例如判别联合中两个分支都使用 value 字段,但一个描述为“邮箱地址”,另一个描述为“手机号”时,错误可能会回退为:
json
{
"message": "value: 最少11个字符"
}如果你希望这类场景也始终输出业务字段名,最稳妥的做法仍然是直接写 message。
如果你在 service、拦截器或自定义流程中手动调用 safeParse,记得把 schema 一起传给 getZodError,否则只能拿到字段路径,拿不到 describe() 标签:
typescript
const result = schema.safeParse(data)
if (!result.success) {
const msg = getZodError(result.error, schema)
}Prisma
PrismaBaseService 和 PrismaRepository 有什么区别?
| 类 | 职责 | 继承方式 |
|---|---|---|
PrismaBaseService<C> | 连接管理 — 生命周期、扩展注册、健康检查 | 每个数据源一个子类 |
PrismaRepository<T> | CRUD 操作 — 通用增删改查方法、分页 | 每个业务 Model 一个子类 |
typescript
// PrismaService:管连接
class PrismaService extends PrismaBaseService<PrismaClient> { ... }
// UserRepository:管业务
class UserRepository extends PrismaRepository<User> {
constructor(prisma: PrismaService) {
super(prisma, 'user')
}
}软删除扩展不生效?
检查以下几点:
- 模型必须有
deletedAt DateTime?字段 PrismaModule.forRoot的middlewares.softDelete.enabled必须为true- 扩展注册在
$connect()之前,确保setClient()在构造函数中调用 models设置为'auto'时,会从 Prisma 运行时模型元数据自动发现,确保prisma generate已执行
@PrismaTransactional 的 prismaField 有什么用?
当你的 Service 中 PrismaClient 的属性名不是 prisma 时,需要通过 prismaField 指定:
typescript
@Injectable()
class OrderService {
// 属性名不是 prisma,而是 db
constructor(private readonly db: PrismaClient) {}
@PrismaTransactional({ prismaField: 'db' })
async createOrder(dto: CreateOrder) {
const client = usePrismaClient(this.db)
// ...
}
}缓存
Redis 断连后会发生什么?
行为取决于 strict 模式配置:
| 模式 | Redis 断连时的行为 |
|---|---|
strict: false(默认) | 抛出 CacheNotReadyException,调用方可 try/catch 降级处理 |
strict: true | 直接抛出异常,请求返回 500 |
CacheService 内置自动重连机制(最多 10 次),健康检查间隔自动检测并恢复连接。
如何在 Service 中安全使用缓存?
typescript
@Injectable()
class UserService {
constructor(private readonly cache: CacheService) {}
async getUser(id: string) {
try {
const cached = await this.cache.get(`user:${id}`)
if (cached) return JSON.parse(cached)
} catch (e) {
// Redis 不可用时变降级:走数据库查询
}
return this.prisma.user.findUnique({ where: { id } })
}
}日志
createLogger() 和 LoggerModule 该用哪个?
| 方式 | 场景 | 优势 |
|---|---|---|
createLogger() + import { logger } | 工具函数、非 DI 上下文 | 简单直接 |
LoggerModule.forRoot() + DI 注入 | Service / Controller | 可测试、可替换 |
两者可以同时使用。LoggerModule 在 onModuleInit 时会自动调用 createLogger() 初始化全局 logger,所以使用 LoggerModule 后不需要再手动调用 createLogger()。
健康检查
如何在 Kubernetes 中配置探针?
yaml
# deployment.yaml
livenessProbe:
httpGet:
path: /health
port: 3000
initialDelaySeconds: 10
periodSeconds: 30
readinessProbe:
httpGet:
path: /health
port: 3000
initialDelaySeconds: 5
periodSeconds: 10健康检查返回格式是什么?
json
{
"status": "ok",
"components": {
"prisma": { "status": "up", "duration": 3 },
"redis": { "status": "up", "duration": 1 }
},
"duration": 5
}status 为 "error" 时,对应组件的 status 为 "down" 并包含 message 字段。