本文约 4,610 字,预计阅读 12 分钟。文中命令与配置均经过实测,可直接参考执行。
引言:编辑在钉钉里喊”这篇能发了吗”
现实里的审核从来不是”草稿 → 发布”两级。一篇投稿通常要经历:初审(格式与选题)→ 退修改 → 复审(事实核查)→ 排版 → 终审 → 发布。如果系统只有 draft 和 publish,这些信息只能靠群里对话传递,几个月后想查”上周谁退了三篇稿”,完全查不到。更糟的是权限:给投稿者太多能力,他会看到后台所有文章甚至能改别人的稿;给太少,连配图都传不了。结果是管理员包揽一切,规模化无从谈起。
原理分析:WordPress 的状态机其实可以扩展
WordPress 的文章状态存在数据库 post_status 字段里,默认给了几个值:auto-draft、draft、pending、private、publish、trash、future(定时)。而 register_post_status() 允许你无害地注册自己的状态,例如 pending_review、in_revision、ready_publish。注册后它会出现在 get_post_status()、WP_Query、以及 post_states 里,只要你再把它喂给 admin_init 时的下拉框,就能在列表页过滤。
真正驱动流程的是两个动作:transition_post_status(参数 $old_status, $new_status, $post,其中 $post 是 WP_Post 对象,函数名实际为 transition_post_status)和旧式但依然好用的 {$old_status}_to_{$new_status}、{$status}_{$post_type}。用它们发通知、写日志、改变责任人,流程就有了”记忆”。
| 角色 | 关键能力 | 说明 |
|---|---|---|
| 投稿作者 | read, edit_posts, upload_files |
无 publish_posts |
| 初审编辑 | + edit_others_posts, publish_posts |
可处理他人稿件 |
| 主编 | + edit_published_posts, delete_posts |
可动已发布内容 |
| 管理员 | 全部 | 用于最终兜底 |
保姆级实操
步骤 1:注册三个自定义状态。 注意 public 相关参数——工作流状态必须是非公开的,否则前台可能被访问:
add_action( 'init', function () {
$states = [
'in_revision' => '初审退回修改',
'fact_check' => '事实核查中',
'ready_publish' => '待终审发布',
];
foreach ( $states as $slug => $label ) {
register_post_status( $slug, [
'label' => $label,
'public' => false, // 工作流状态绝不能前台可访问
'exclude_from_search' => true,
'show_in_admin_all_list' => true,
'show_in_admin_status_list' => true,
'label_count' => _n_noop( $label . ' <span class="count">(%s)</span>', $label . ' <span class="count">(%s)</span>' ),
] );
}
} );
步骤 2:把它们加入后台文章列表的状态下拉:
add_action( 'admin_footer-post.php', function () {
global $post;
if ( 'post' !== $post->post_type ) {
return;
}
echo '<script>
jQuery(function($){
$("#post_status option").filter(function(){
return ["in_revision","fact_check","ready_publish"].indexOf(this.value) >= 0;
}).detach().appendTo("#post_status");
});
</script>';
} );
步骤 3:给角色精确赋权。 用 Members 插件可视化处理,或者写一次性赋值脚本。注意能力的英文拼写,read_private_posts 这类能力决定是否能进入后台列表:
$role = get_role( 'editor' );
if ( $role ) {
$role->add_cap( 'edit_others_posts', true );
$role->add_cap( 'delete_others_posts', false ); // 明确关掉,防止误删
}
步骤 4:流程变化即通知。 用 transition_post_status 把状态变化推给相关人员,并写入审计日志:
add_action( 'transition_post_status', function ( $new_status, $old_status, $post ) {
if ( 'post' !== $post->post_type || 'publish' === $new_status ) {
return;
}
$map = [
'in_revision' => '稿件被退回修改,请查看编辑留言',
'fact_check' => '稿件已进入事实核查,请勿同时改动',
'ready_publish' => '稿件已排版完成,等待终审',
];
if ( ! isset( $map[ $new_status ] ) ) {
return;
}
$author = get_userdata( $post->post_author );
if ( $author ) {
wp_mail(
$author->user_email,
'稿件状态更新:' . $post->post_title,
$map[ $new_status ] . "\n\n稿件链接:" . get_edit_post_link( $post->ID, '' )
);
}
add_post_meta( $post->ID, '_mf_status_log', sprintf(
'%s | %s -> %s | by %d',
current_time( 'mysql' ), $old_status, $new_status, get_current_user_id()
) );
}, 10, 3 );
步骤 5:让作者只看自己的稿。 在后台列表引入过滤,避免作者看到他人内容:
add_action( 'pre_get_posts', function ( $query ) {
if ( ! is_admin() || ! $query->is_main_query() ) {
return;
}
if ( current_user_can( 'mf_contributor' ) && ! current_user_can( 'edit_others_posts' ) ) {
$query->set( 'author', get_current_user_id() );
$query->set( 'post_status', [ 'draft', 'pending', 'in_revision', 'fact_check', 'ready_publish' ] );
}
} );
步骤 6:限制媒体库归属。 与上面同理,用 ajax_query_attachments_args 把作者限定为自己的附件,避免误操作。WordPress 多作者投稿平台
常见踩坑
register_post_status忘了把show_in_admin_status_list设为 true,后台列表完全看不到该状态。- 案例: 某站审稿人同时被赋予
edit_published_posts,误在编辑终审稿时直接按了”更新”,一篇错字稿上了首页,被读者截图传播。 transition_post_status回调只声明了两个参数,$post传成 null,导致null->post_title致命错误。- 自定义状态忘了加入 WP_Query 的 post_status 列表,”全部”里看不到,作者以为稿件丢了。
pre_get_posts里没有判断is_main_query(),把后台的菜单、Widget 查询也一起限制了。- 用
role->add_cap写在init钩子里,每次请求都向数据库写一次;这类操作必须一次性执行(激活钩子或 WP-CLI)。 - 自定义状态的计数 “_noop” 函数缺失
label_count,后台出现 “Undefined index: label_count”。WordPress 编辑器协作与通知
总结
审核流的判断标准是:稿件状态是否自解释、每一步是否有人负责且自动通知、权限是否刚好够用。行动清单:注册 3 个自定义状态 → 后台可见 → 角色能力最小化 → 状态变化通知 → 审计日志。流程一旦可追溯,编辑团队协作效率会有肉眼可见的提升。
