在处理 Payload 项目(payload.config.ts、集合、字段、钩子、访问控制、Payload API)时使用。在调试验证错误、安全问题、关系查询、事务或钩子行为时使用。
Payload 应用开发
Payload 是一个基于 Next.js 的原生 CMS,采用 TypeScript 优先架构,提供管理面板、数据库管理、REST/GraphQL API、身份验证和文件存储。
快速参考
快速开始
npx create-payload-app@latest my-app
cd my-app
pnpm dev
最小配置
import { buildConfig } from 'payload'
import { mongooseAdapter } from '@payloadcms/db-mongodb'
import { lexicalEditor } from '@payloadcms/richtext-lexical'
import path from 'path'
import { fileURLToPath } from 'url'
const filename = fileURLToPath(import.meta.url)
const dirname = path.dirname(filename)
export default buildConfig({
admin: {
user: 'users',
importMap: {
baseDir: path.resolve(dirname),
},
},
collections: [Users, Media],
editor: lexicalEditor(),
secret: process.env.PAYLOAD_SECRET,
typescript: {
outputFile: path.resolve(dirname, 'payload-types.ts'),
},
db: mongooseAdapter({
url: process.env.DATABASE_URL,
}),
})
基本模式
默认值与约定
在建模内容时应用以下默认值,除非有明确理由不这样做:
- 默认启用草稿/版本:
versions: { drafts: true }。这是任何内容集合的推荐起点。它会自动注入一个_status字段(draft/published/changed)——不要添加自己的status字段,这是多余的。仅对没有发布/草稿生命周期的集合(例如内部连接表、设置)跳过版本。 - 对所有 slug 使用
slugField(),而不是手动编写{ name: 'slug', type: 'text', unique: true }。它会从标题自动生成 slug,添加重新生成开关,并为你处理唯一性和索引。默认从title字段生成——如果集合没有title,则传入源字段:slugField({ useAsSlug: 'name' })。 position: 'sidebar'用于简短、一目了然的字段——状态、分类、作者、发布日期。避免将其用于需要水平空间才能使用的长字段(描述、富文本内容、长文本)。这些属于主文档区域。
基本集合
import type { CollectionConfig } from 'payload'
import { slugField } from 'payload'
export const Posts: CollectionConfig = {
slug: 'posts',
admin: {
useAsTitle: 'title',
// _status(来自 versions.drafts)显示草稿/发布状态——无需自定义 status 字段
defaultColumns: ['title', 'author', '_status', 'createdAt'],
},
versions: {
drafts: true,
},
fields: [
{ name: 'title', type: 'text', required: true },
slugField(), // 从 `title` 自动生成,唯一 + 索引,侧边栏位置
{ name: 'content', type: 'richText' }, // 长字段——留在主区域,不在侧边栏
// 简短、一目了然的字段——适合侧边栏
{ name: 'author', type: 'relationship', relationTo: 'users', admin: { position: 'sidebar' } },
],
timestamps: true,
}
有关更多集合模式(身份验证、上传、草稿、实时预览),请参阅 COLLECTIONS.md。
常见字段
// 文本字段
{ name: 'title', type: 'text', required: true }
// 关系
{ name: 'author', type: 'relationship', relationTo: 'users', required: true }
// 富文本
{ name: 'content', type: 'richText', required: true }
// Slug——使用辅助函数而不是手动编写文本字段
slugField()
// 选择(用于真正的分类——不是发布状态;发布状态使用 versions.drafts + _status)
{ name: 'category', type: 'select', options: ['news', 'tutorial', 'opinion'] }
// 上传
{ name: 'image', type: 'upload', relationTo: 'media' }
有关所有字段类型(数组、块、点、连接、虚拟、条件等),请参阅 FIELDS.md。
钩子示例
钩子存在于两个级别之一,且不可互换。集合钩子接收 { doc, data, req, operation, ... } 并对整个文档进行操作。字段钩子位于单个字段的 hooks 对象内部,接收 { value, siblingData, ... },并返回该字段的新值。计算/虚拟字段、每个字段的格式化程序和每个字段的访问掩码是字段钩子;跨字段业务逻辑是集合钩子。
// 集合级别:跨文档的业务逻辑
export const Posts: CollectionConfig = {
slug: 'posts',
hooks: {
beforeChange: [
async ({ data, operation }) => {
if (operation === 'create') {
data.slug = slugify(data.title)
}
return data
},
],
},
fields: [{ name: 'title', type: 'text' }],
}
// 字段级别:计算/格式化单个字段的值(虚拟字段使用此方式)
export const Users: CollectionConfig = {
slug: 'users',
fields: [
{ name: 'firstName', type: 'text' },
{ name: 'lastName', type: 'text' },
{
name: 'fullName',
type: 'text',
virtual: true,
hooks: {
afterRead: [({ siblingData }) => `${siblingData.firstName} ${siblingData.lastName}`],
},
},
],
}
当要求“计算字段”或“在钩子中填充字段的值”时,使用该字段上的字段级钩子——永远不要使用修改 doc 的集合级 afterRead。
有关所有钩子模式,请参阅 HOOKS.md。有关访问控制,请参阅 ACCESS-CONTROL.md。
类型安全的访问控制
import type { Access } from 'payload'
import type { User } from '@/payload-types'
// 类型安全的访问控制
export const adminOnly: Access = ({ req }) => {
const user = req.user as User
return user?.roles?.includes('admin') || false
}
// 行级访问控制
export const ownPostsOnly: Access = ({ req }) => {
const user = req.user as User
if (!user) return false
if (user.roles?.includes('admin')) return true
return {
author: { equals: user.id },
}
}
查询示例
// 本地 API
const posts = await payload.find({
collection: 'posts',
where: {
status: { equals: 'published' },
'author.name': { contains: 'john' },
},
depth: 2,
limit: 10,
sort: '-createdAt',
})
// 查询并填充关系
const post = await payload.findByID({
collection: 'posts',
id: '123',
depth: 2, // 填充关系(默认为 2)
})
// 返回:{ author: { id: "user123", name: "John" } }
// 无 depth 时,关系仅返回 ID
const post = await payload.findByID({
collection: 'posts',
id: '123',
depth: 0,
})
// 返回:{ author: "user123" }
有关所有查询运算符和 REST/GraphQL 示例,请参阅 QUERIES.md。
获取 Payload 实例
// 在 API 路由中(Next.js)
import { getPayload } from 'payload'
import config from '@payload-config'
export async function GET() {
const payload = await getPayload({ config })
const posts = await payload.find({
collection: 'posts',
})
return Response.json(posts)
}
// 在服务器组件中
import { getPayload } from 'payload'
import config from '@payload-config'
export default async function Page() {
const payload = await getPayload({ config })
const { docs } = await payload.find({ collection: 'posts' })
return <div>{docs.map(post => <h1 key={post.id}>{post.title}</h1>)}</div>
}
安全陷阱
1. 本地 API 访问控制(关键)
默认情况下,本地 API 操作会绕过所有访问控制,即使传递了用户也是如此。
// ❌ 安全漏洞:传递了用户但忽略了其权限
await payload.find({
collection: 'posts',
user: someUser, // 访问控制被绕过!
})
// ✅ 安全:实际强制执行用户权限
await payload.find({
collection: 'posts',
user: someUser,
overrideAccess: false, // 访问控制必需
})
何时使用每种方式:
overrideAccess: true(默认)——你信任的服务器端操作(定时任务、系统任务)overrideAccess: false——代表用户操作时(API 路由、Webhook)
请参阅 QUERIES.md#access-control-in-local-api。
2. 钩子中的事务失败
钩子中没有 req 的嵌套操作会破坏事务原子性。
// ❌ 数据损坏风险:单独的事务
hooks: {
afterChange: [
async ({ doc, req }) => {
await req.payload.create({
collection: 'audit-log',
data: { docId: doc.id },
// 缺少 req - 在单独的事务中运行!
})
},
]
}
// ✅ 原子性:同一事务
hooks: {
afterChange: [
async ({ doc, req }) => {
await req.payload.create({
collection: 'audit-log',
data: { docId: doc.id },
req, // 保持原子性
})
},
]
}
请参阅 ADAPTERS.md#threading-req-through-operations。
3. 无限钩子循环
触发相同钩子的操作会导致无限循环。
// ❌ 无限循环
hooks: {
afterChange: [
async ({ doc, req }) => {
await req.payload.update({
collection: 'posts',
id: doc.id,
data: { views: doc.views + 1 },
req,
}) // 再次触发 afterChange!
},
]
}
// ✅ 安全:使用上下文标志
hooks: {
afterChange: [
async ({ doc, req, context }) => {
if (context.skipHooks) return
await req.payload.update({
collection: 'posts',
id: doc.id,
data: { views: doc.views + 1 },
context: { skipHooks: true },
req,
})
},
]
}
请参阅 HOOKS.md#context。
项目结构
src/
├── app/
│ ├── (frontend)/
│ │ └── page.tsx
│ └── (payload)/
│ └── admin/[[...segments]]/page.tsx
├── collections/
│ ├── Posts.ts
│ ├── Media.ts
│ └── Users.ts
├── globals/
│ └── Header.ts
├── components/
│ └── CustomField.tsx
├── hooks/
│ └── slugify.ts
└── payload.config.ts
构建与类型生成
Payload 会为你生成 payload-types.ts——你很少需要手动运行 generate:types。
- 开发期间:
typescript.autoGenerate默认为true,因此开发服务器会在配置更改时自动重新生成类型。不要在开发服务器运行时手动运行generate:types——这是多余的。 - 构建期间:
payload build会在运行next build之前生成导入映射和类型。优先使用它而不是直接调用next build,这样两者都不会过时。传递--no-types以跳过类型生成。 - 手动生成(
payload generate:types)是一个逃生舱——仅当开发服务器和构建都不在循环中时使用(例如一次性脚本,或在运行payload build之前的 CI 步骤中)。
// payload.config.ts
export default buildConfig({
typescript: {
outputFile: path.resolve(dirname, 'payload-types.ts'),
// autoGenerate 默认为 true——开发中类型自动重新生成
},
})
// 使用
import type { Post, User } from '@/payload-types'
常见陷阱
- 本地 API 绕过访问控制,除非传递
overrideAccess: false - 嵌套操作中缺少
req会破坏事务原子性 - 钩子循环——钩子中的操作可能重新触发相同的钩子;使用
req.context标志 - 字段级访问仅返回布尔值,无查询约束
- 关系深度默认为 2;设置
depth: 0仅获取 ID - 草稿状态——启用草稿时自动注入
_status字段 - 类型在开发中自动重新生成(
autoGenerate)和payload build期间——避免手动运行generate:types - MongoDB 事务需要副本集配置
- SQLite 事务默认禁用;使用
transactionOptions: {}启用 - Point 字段在 SQLite 中不受支持
最佳实践
内容建模
- 默认在内容集合上启用
versions: { drafts: true };依赖自动注入的_status字段,而不是添加自定义的status字段 - 使用
slugField()处理 slug,而不是手动编写唯一文本字段 - 将
position: 'sidebar'保留给简短、一目了然的字段(状态、分类、作者、日期);将长字段(描述、富文本)保留在主区域
安全
- 默认限制性访问,逐步添加权限
- 将
user传递给本地 API 时使用overrideAccess: false - 字段级访问仅返回布尔值(无查询约束)
- 永远不要信任客户端提供的数据
- 对角色使用
saveToJWT: true以避免数据库查找
性能
- 为频繁查询的字段建立索引
- 使用
select限制返回的字段 - 在关系上设置
maxDepth以防止过度获取 - 在访问控制中优先使用查询约束而不是异步操作
- 在
req.context中缓存昂贵的操作
数据完整性
- 始终将
req传递给钩子中的嵌套操作 - 使用上下文标志防止无限钩子循环
- 为 MongoDB(需要副本集)和 Postgres 启用事务
- 使用
beforeValidate进行数据格式化 - 使用
beforeChange进行业务逻辑
类型安全
- 让开发(
autoGenerate)和payload build生成类型;仅当两者都不运行时才手动运行generate:types - 从生成的
payload-types.ts导入类型 - 为用户对象添加类型:
import type { User } from '@/payload-types' - 使用字段类型守卫进行运行时类型检查
- 当将任何 Payload 值提取为命名常量时——集合、字段、钩子、访问函数、插件等——使用匹配的 Payload 类型(
CollectionConfig、Field、CollectionBeforeChangeHook、Access、Plugin……)进行注释,或使用satisfies <Type>。没有注释时,像type: 'text'这样的字符串属性会拓宽为string,区分联合(Field、CollectionConfig)无法解析。内联字面量通过上下文类型自动获得此功能;提取的常量则不会。
组织
- 将集合放在单独的文件中
- 将访问控制提取到
access/目录 - 将钩子提取到
hooks/目录 - 对常见模式使用可重用的字段工厂
- 用注释记录复杂的访问控制
参考文档
- FIELDS.md - 所有字段类型、验证、管理选项
- FIELD-TYPE-GUARDS.md - 用于运行时字段类型检查和收窄的类型守卫
- COLLECTIONS.md - 集合配置、身份验证、上传、草稿、实时预览
- HOOKS.md - 集合钩子、字段钩子、上下文模式
- ACCESS-CONTROL.md - 集合、字段、全局访问控制、RBAC、多租户
- ACCESS-CONTROL-ADVANCED.md - 上下文感知、基于时间、基于订阅的访问、工厂函数、模板
- QUERIES.md - 查询运算符、本地/REST/GraphQL API
- ENDPOINTS.md - 自定义 API 端点:身份验证、辅助函数、请求/响应模式
- ADAPTERS.md - 数据库、存储、电子邮件适配器、事务
- ADVANCED.md - 身份验证、作业、端点、组件、插件、本地化
- PLUGIN-DEVELOPMENT.md - 插件架构、单体仓库结构、模式、最佳实践






