跳转到内容

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

zVarCharzStr 和字符串 helper 怎么选?

预设必填长度约束典型场景
zStr选填搜索关键词、普通可选文本
zStrReq必填无长度限制的必填字段
zStrNull可空前端空串表示“清空为 null”
zVarChar(n)必填max=n对齐 @db.VarChar(n)
zVarCharNull(n)可空max=n对齐 String? @db.VarChar(n) 且空串→null
zVarCharOptional(n)选填max=n兼容旧 API,推荐新代码优先用语义更明确的 helper

快速判断:

  • 空串应该视为“没传”:用 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: '请输入有效的邮箱地址' })
)

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' 时,会从 DMMF 自动发现,确保 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 字段。