跳转到内容

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 预处理器(如 zIntzNumzBool)会自动将 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`

zVarCharzStr 和字符串 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”:用 zStrNullzVarCharNull
  • 字段要对齐 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: '请输入有效的邮箱地址' })
)

为什么返回的校验错误看起来只有字段名,没有业务提示?

默认情况下,错误前缀来自字段路径,例如 usernameprofile.nickname,而错误正文来自两部分:

  • Zod 内置规则的默认翻译,例如 最少2个字符邮箱格式不正确
  • 你在规则里显式传入的 message

如果你没有写自定义 message,也没有给字段 schema 添加 describe(),最终看到的通常就是:

json
{
  "message": "username: 最少2个字符"
}

如果希望提示更像业务文案,推荐分两层处理:

  1. describe() 提供字段中文名
typescript
const CreateUserSchema = z.object({
  username: zStr.pipe(z.string().min(2)).describe('用户名'),
  email: zStr.pipe(z.string().email()).describe('邮箱')
})

这样会优先显示:

json
{
  "message": "用户名: 最少2个字符"
}
  1. 对关键业务规则显式写 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() 作为字段标签
  • 仍取不到时,回退为原始字段路径

适用范围:

  • 普通对象字段、嵌套对象字段
  • 数组元素字段
  • pipepreprocesstransformoptionalnullable 等包装层
  • lazy 递归 schema
  • union / discriminatedUnion 中标签唯一的字段路径
  • 库内公开 helper 与组合器,如 zVarCharNullzQueryEnumIntzPageWithKeywordzDateRange

如果是联合类型,还要注意一条规则:

  • 同一路径在多个分支下如果对应不同的 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

PrismaBaseServicePrismaRepository 有什么区别?

职责继承方式
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')
  }
}

软删除扩展不生效?

检查以下几点:

  1. 模型必须有 deletedAt DateTime? 字段
  2. PrismaModule.forRootmiddlewares.softDelete.enabled 必须为 true
  3. 扩展注册在 $connect() 之前,确保 setClient() 在构造函数中调用
  4. models 设置为 'auto' 时,会从 Prisma 运行时模型元数据自动发现,确保 prisma generate 已执行

@PrismaTransactionalprismaField 有什么用?

当你的 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可测试、可替换

两者可以同时使用。LoggerModuleonModuleInit 时会自动调用 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 字段。