子比主题开发文档
使用指南Codestar Framework主题扩展在线部署AI 功能推荐插件赞助打赏

论坛模块

扩展子比主题论坛版块、帖子、话题、标签、评分、采纳、版主、论坛页面和论坛 Ajax 动作。

模块入口

论坛模块入口是 inc/functions/bbs/bbs.php。它先定义资源常量,再加载基础函数和动作:

define('ZIB_BBS_ASSETS_URI', ZIB_TEMPLATE_DIRECTORY_URI . '/inc/functions/bbs/assets');
define('ZIB_BBS_REQUIRE_URI', '/inc/functions/bbs/');

zib_require(array(
    'inc/functions',
    'action/action',
), false, ZIB_BBS_REQUIRE_URI);

后台配置总是会加载 inc/functions/bbs/admin/option.php。真正的论坛前台能力由 zib_bbs() 的状态控制:

$zib_bbs = zib_bbs();
if (!$zib_bbs->s) {
    // 论坛关闭时,后台评论列表默认不显示论坛评论。
} else {
    zib_require(array(
        'widgets/widgets',
        'inc/user-page',
    ), false, ZIB_BBS_REQUIRE_URI);
}

扩展论坛前先判断论坛是否开启,不要假设 plateforum_post 和论坛模板一定参与当前请求。

目录地图

目录作用
inc/functions/bbs/inc/class.init.php注册 plateforum_postplate_catforum_topicforum_tag,处理重写规则和主查询
inc/functions/bbs/inc/functions.php论坛基础函数、列表、推荐指数、热度、SEO、浏览统计
inc/functions/bbs/inc/plate.php版块、版块关注、付费关注、版块页面
inc/functions/bbs/inc/posts.php帖子列表、搜索、动态推荐分、收藏、排序
inc/functions/bbs/inc/comment.php回答、采纳、热评、评论操作扩展
inc/functions/bbs/inc/edit.php论坛发布和编辑表单能力
inc/functions/bbs/inc/template.php论坛通用页面骨架和动态模板 Hook
inc/functions/bbs/action/*前台论坛 Ajax 动作
inc/functions/bbs/admin/*论坛后台设置、Meta、批量编辑
inc/functions/bbs/widgets/*论坛小工具和论坛侧栏

数据模型

inc/functions/bbs/inc/class.init.php 注册论坛的核心对象:

类型注册方式说明
plateregister_post_type()版块
forum_postregister_post_type()帖子
plate_catregister_taxonomy('plate_cat', array('plate'), ...)版块分类
forum_topicregister_taxonomy('forum_topic', array('forum_post'), ...)话题
forum_tagregister_taxonomy('forum_tag', array('forum_post'), ...)标签

论坛不是普通文章列表的换皮。它额外处理:

  • 版块关注、付费关注和查看权限。
  • 帖子评分、精华、置顶、热帖、动态推荐分。
  • 评论采纳、热评、回答身份标记。
  • 版主申请、添加、移除、审核。
  • 论坛首页、版块页、话题页、标签页和帖子编辑页的模板路由。

搜索与新增入口

论坛开启后会把版块和帖子加入主题搜索:

add_filter('search_types', 'zib_bbs_search_types_filter');
add_filter('search_main_tabs_array', 'zib_bbs_search_main_tabs_array_filter');

也会把创建版块、创建话题、发布帖子加入顶部新增按钮选项:

add_filter('new_add_btns_bbs_plate', 'zib_bbs_new_add_btns_filter_bbs_plate');
add_filter('new_add_btns_bbs_topic', 'zib_bbs_new_add_btns_filter_bbs_topic');
add_filter('new_add_btns_bbs_posts', 'zib_bbs_new_add_btns_filter_bbs_posts');
add_filter('new_add_btns_options', 'zib_bbs_new_add_btns_options');

扩展搜索时优先挂这些 Filter,而不是直接改搜索模板。

搜索页实际会联动三处论坛实现:

环节文件说明
类型和 Tabinc/functions/bbs/bbs.php注册 plateforum 两个搜索类型
主查询inc/functions/bbs/inc/class.init.phptype=forum 设置 post_type=forum_posttype=plate 设置 post_type=plate
内容输出inc/functions/bbs/inc/posts.phpinc/functions/bbs/inc/plate.php分别渲染帖子搜索结果和版块搜索结果

论坛搜索的版块筛选使用 trem=plate_{plate_id},再由 main_post_query() 追加 plate_id meta 查询。话题、标签和版块分类仍然走普通 term ID。更完整的搜索筛选、排序、facet 缓存和 REST 搜索边界见 搜索体系

前台模板 Hook

论坛模板骨架来自 inc/functions/bbs/inc/template.phpinc/functions/bbs/page/*。模板会按页面类型触发动态 Hook:

do_action('bbs_locate_template');
do_action('bbs_locate_template_' . $type);

do_action('bbs_' . $page_type . '_page_header');
do_action('bbs_' . $page_type . '_page_content');
do_action('bbs_' . $page_type . '_page_sidebar');
do_action('bbs_' . $page_type . '_page_footer');

常见页面类型:

页面常见 Hook
论坛首页bbs_home_page_contentbbs_home_tab_content_topbbs_home_tab_content_bottom
版块页bbs_plate_page_contentbbs_plate_page_sidebar
帖子页bbs_posts_page_contentbbs_single_footerzib_bbs_posts_content_after
发帖/编辑页bbs_posts_edit_page_contentbbs_posts_edit_page_sidebar
话题/标签页bbs_forum_topic_page_contentbbs_forum_tag_page_content

追加模块时优先使用 Hook:

add_action('bbs_single_footer', 'zib_docs_bbs_single_notice', 20);

function zib_docs_bbs_single_notice($post)
{
    if (empty($post->ID) || $post->post_type !== 'forum_post') {
        return;
    }

    echo '<div class="muted-box mt20">' . esc_html__('请遵守社区交流规则。', 'zib_language') . '</div>';
}

论坛首页与 Tab 配置

论坛首页不是普通页面正文承载,而是由 inc/functions/bbs/page/home.php 进入 zib_bbs_page_template('home')。页面骨架会按 home 类型输出三组动态侧栏和四组页面 Hook:

位置入口
顶部通栏dynamic_sidebar('bbs_home_top_fluid')
页面头部do_action('bbs_home_page_header')
主内容区do_action('bbs_home_page_content')
右侧栏dynamic_sidebar('bbs_home_sidebar')do_action('bbs_home_page_sidebar')
底部通栏dynamic_sidebar('bbs_home_bottom_fluid')
页面底部do_action('bbs_home_page_footer')

首页主内容默认挂载 zib_bbs_home_tab_content()。它读取后台 bbs_home_tab 配置,并先经过 bbs_home_tab_options Filter,再生成 Tab 导航和 Tab 内容:

$tabs_options = apply_filters('bbs_home_tab_options', _pz('bbs_home_tab', array()));

管理员和论坛管理员会自动追加 pending Tab,用于查看待审核帖子。普通用户不会看到这个入口。每个 Tab 都可以通过 ?index=2?index=3 这样的参数直达,默认激活项来自 bbs_home_tab_active_index

首页 Tab 的外层结构也很重要:

echo '<div class="fixed-wrap bbs-home-tab">';
echo '<div class="fixed-wrap-nav zib-widget affix-header-sm" offset-top="-8">' . $tab_nav . '</div>';
echo '<div class="fixed-wrap-content">';
do_action('bbs_home_tab_content_top');
echo $tab_content;
do_action('bbs_home_tab_content_bottom');
echo '</div>';
echo '</div>';

因此扩展首页模块时,优先放在 bbs_home_tab_content_topbbs_home_tab_content_bottom,不要重写整套首页模板。这样可以继续复用主题的吸顶导航、Ajax Tab、骨架屏和分页加载。

首页 Tab 数据结构

bbs_home_tab 是 Codestar sortable 字段。默认包含关注、综合、版块和多组帖子列表类栏目。常见 key 可以按这个方式理解:

Key内容来源说明
followbbs_home_tab_content_follow当前用户关注版块内的帖子,未登录时显示登录提示
synthesisbbs_home_tab_content_synthesis综合帖子列表,最终走帖子查询
platebbs_home_tab_content_plateplate_cat 分组展示版块
tabs / tabs_2 / tabs_3bbs_home_tab_content_other后台配置的帖子列表栏目,如热门、精华、问答、投票、最新回复
pendingbbs_home_tab_content_pending管理员和论坛管理员的待审核列表

如果只是追加一个首页 Tab,可以改 bbs_home_tab_options。示例保留主题的命名函数和 array() 写法:

add_filter('bbs_home_tab_options', 'zib_docs_bbs_home_tab_options', 20);

function zib_docs_bbs_home_tab_options($tabs_options)
{
    if (!is_array($tabs_options)) {
        $tabs_options = array();
    }

    $tabs_options['tabs'][] = array(
        'show'      => array('pc_s', 'm_s'),
        'title'     => __('最新讨论', 'zib_language'),
        'style'     => 'detail',
        'orderby'   => 'modified',
        'filter'    => array(),
        'topping_s' => true,
    );

    return $tabs_options;
}

如果只是给首页 Tab 内容上方加一个轻量提示,使用 Action 更合适:

add_action('bbs_home_tab_content_top', 'zib_docs_bbs_home_notice');

function zib_docs_bbs_home_notice()
{
    echo '<div class="zib-widget mb20">' . esc_html__('欢迎参与社区讨论。', 'zib_language') . '</div>';
}

版块分区与排序

首页 plate Tab 由 zib_bbs_get_home_tab_content_plate() 渲染。它先判断当前用户是否有创建版块权限:

$can_plate_add = zib_bbs_current_user_can('plate_add');

然后读取后台配置:

配置用途
cat_orderby控制版块分类 plate_cat 的排序方式
orderby_includecat_orderby=include 时,手动指定并排序分类
orderby控制分类内版块列表的排序方式
user_follow在版块分区前展示当前用户关注的版块
hot_plate在版块分区前展示系统推荐版块

分类查询入口是:

$cat_objs = zib_bbs_get_plate_cats_orderby($orderby, !$can_plate_add, $include);

这里第二个参数会根据创建版块权限决定是否隐藏空分类:没有创建权限的用户不需要看到空分区;有创建权限的用户可以看到空分区并通过创建按钮补齐版块。

每个分类会渲染为 .panel.panel-plate。桌面端标题是可折叠结构,移动端只输出标题,分类内列表统一交给:

zib_bbs_get_plate_main_lists(array(
    'cat'     => $cat_obj->term_id,
    'orderby' => $plate_orderby,
));

zib_bbs_get_plate_main_lists() 内部继续走 zib_bbs_get_plate_query(),所以新增排序或筛选时应该扩展查询参数或排序选项,不要另写一套版块 HTML。版块查询默认是 post_type=platepost_status=publishshowposts=-1,传入 cat 时会转成 plate_cattax_query,传入 include 时会使用 post__inorderby=post__in 保留手动排序。

首页版块内容前后还各有一个 Filter:

apply_filters('bbs_home_tab_content_plate_before', $html, $option);
apply_filters('bbs_home_tab_content_plate_after', $html, $option);

主题内置的“我关注的版块”和“热门推荐”就是挂在 bbs_home_tab_content_plate_before 上。自定义推荐版块时也建议挂这里,并保持输出为 panel panel-plate,这样视觉和折叠布局会跟原生分区一致。

帖子列表 Tab

synthesisother 和后台多组帖子列表栏目最终会进入 zib_bbs_get_home_tab_content_other()。它支持的核心筛选参数包括:

参数说明
include_plate只看指定版块
exclude_plate排除指定版块;存在 include_plate 时不会生效
include_topic只看指定话题
include_tag只看指定标签
orderby帖子排序方式
bbs_type帖子类型筛选
filter其它筛选,如精华、热门等
allow_view阅读权限筛选
style列表样式,detailmini
topping_s首页第一页是否显示置顶帖

这个函数会按配置决定是否输出版块头、话题头或标签头,再通过 zib_bbs_get_posts_query() 查询帖子。分页使用 zib_bbs_get_paginate($posts->found_posts),并追加 .post_ajax_loader 骨架屏。扩展帖子列表 Tab 时,重点是补齐查询参数,不要破坏 .ajax-item.ajax-pag.post_ajax_loader 这些 Ajax 分页协议。

快速发布与待审核

首页快速发布由 bbs_home_tab_quick_posts_s 控制,只在第一页生效。主题会用 bbs_home_tab_quick_posts_tab 和当前 index 判断是否插入:

$html = zib_bbs_edit::quick_posts(array(
    'class' => 'ajax-item-header zib-widget',
)) . $html;

如果开启快速发布,主题还会在底部输出 TinyMCE 全局配置:

add_action('wp_print_footer_scripts', 'zib_echo_mce_global_config_script', 5);

待审核 Tab 只对超级管理员或论坛管理员追加,查询 post_status=pending,列表使用 zib_bbs_get_posts_manage_list('alone ajax-item', true)。因此二次开发时不要把待审核入口写死到前台菜单里,应交给主题的身份判断和 pending Tab 自动注入。

发帖与编辑保存链路

论坛发帖页不是普通文章投稿页的换皮,而是独立的 forum_post 编辑链路。模板入口在 inc/functions/bbs/inc/edit-posts.php,主内容和侧栏分别挂到:

add_action('bbs_posts_edit_page_content', 'zib_bbs_posts_edit_page_content');
add_action('bbs_posts_edit_page_sidebar', 'zib_bbs_posts_edit_sidebar');

主内容区负责封面、标题、编辑器和提交提示:

echo zib_bbs_edit::posts_featured($in_args['post_id']);
echo zib_bbs_edit::posts_title($in_args['title']);
zib_bbs_edit::posts_editor($in_args);
echo zib_bbs_edit::desc_text($in_args);

侧栏负责真正会影响保存数据的业务字段:

组件输出函数保存目标
提交按钮zib_bbs_edit::posts_submit()bbs_posts_savebbs_posts_draft
状态切换zib_bbs_edit::status()post_status,仅有审核权限时显示
阅读权限zib_bbs_edit::allow_view_set()allow_viewallow_view_rolesposts_zibpay
版块选择zib_bbs_edit::plate_select()plate_id meta
话题选择zib_bbs_edit::topic_select()forum_topic taxonomy
标签选择zib_bbs_edit::tag_select()forum_tag taxonomy
发布类型zib_bbs_edit::type_select()bbs_type meta
投票设置zib_bbs_edit::vote_set()投票相关 meta

编辑器能力由当前用户权限动态打开。posts_editor() 会按能力挂载 tinymce_upload_imgtinymce_hidetinymce_upload_videotinymce_upload_filetinymce_iframe_video,因此扩展编辑器按钮时要跟随 zib_bbs_current_user_can(),不要全局给所有论坛编辑器打开上传或 iframe 能力。

if ((!$in_args['post_id'] && zib_bbs_current_user_can('posts_upload_img')) || ($in_args['post_id'] && zib_bbs_current_user_can('posts_upload_img', $in_args['post_id']))) {
    add_filter('tinymce_upload_img', '__return_true');
}

保存入口集中在 zib_bbs_ajax_edit_posts()

add_action('wp_ajax_bbs_posts_draft', 'zib_bbs_ajax_edit_posts');
add_action('wp_ajax_bbs_posts_save', 'zib_bbs_ajax_edit_posts');
add_action('wp_ajax_nopriv_bbs_posts_draft', 'zib_bbs_ajax_edit_posts');
add_action('wp_ajax_nopriv_bbs_posts_save', 'zib_bbs_ajax_edit_posts');

实际保存顺序如下:

  1. 校验登录、bbs_edit_posts nonce 和人机验证。
  2. 新帖检查 posts_add,编辑检查 posts_edit
  3. select_plate 校验当前用户是否能发到所选版块。
  4. 校验标题必填、标题长度和发布时的最小长度。
  5. 根据 posts_save_audit_noposts_auditaudit_bbs_postsposts_save_audit_no_manual 判断是否直接发布。
  6. 快速发布时把上传图片写成正文 <img data-edit-file-id="...">,把隐藏内容拼成 [hidecontent] 结构。
  7. 构造 forum_post$insert_args,新帖默认 comment_status=open
  8. 允许 iframe 视频的用户临时放开 wp_kses_allowed_html 的 iframe 属性。
  9. 触发 zib_pre_insert_post,再调用 wp_insert_post()
  10. 保存话题、标签、发布类型、投票、阅读权限和封面。
  11. 触发 bbs_add_postsbbs_edit_posts,并按 pendingdraftpublish 返回不同提示和跳转。

这段链路里最容易写错的是状态判断。bbs_posts_draft 默认保存为 draftbbs_posts_save 默认先走 pending,只有具备免审核能力,或 API 审核通过且具备免人工审核能力,才会变成 publish。有审核权限的人编辑帖子时,侧栏的 post_status 才会生效。

if ('bbs_posts_save' === $action) {
    $post_status = 'pending';

    if ($is_audit) {
        $post_status = !empty($_REQUEST['post_status']) ? $_REQUEST['post_status'] : 'publish';
    }
}

保存后的扩展应挂在主题 Hook 上,而不是替换 Ajax。比如给新帖追加一个已处理标记:

add_action('bbs_add_posts', 'zib_docs_bbs_after_add_posts');

function zib_docs_bbs_after_add_posts($post)
{
    if (empty($post->ID) || get_post_type($post->ID) !== 'forum_post') {
        return;
    }

    zib_update_post_meta($post->ID, 'docs_checked', current_time('mysql'));
}

如果要在入库前做额外校验或记录即将写入的数据,可以监听 zib_pre_insert_post。注意它拿到的是 $insert_args,不应把它当成稳定的字段改写入口;涉及发帖成功后的通知、索引同步、附件归属和缓存清理,优先放到 bbs_add_posts / bbs_edit_posts 后置 Hook。

阅读限制与付费可见

论坛阅读限制同时作用于 plateforum_post。实际判断入口集中在 inc/functions/bbs/inc/posts.php

函数作用
zib_bbs_get_allow_view_data($post)读取 allow_viewallow_view_roles 和付费参数,返回是否开放、允许原因、提示 HTML
zib_bbs_get_posts_not_allow_view($post, $hide_pay)帖子列表和正文判断是否隐藏,先判断所属版块,再判断帖子自身
zib_bbs_get_plate_not_allow_view($post)版块页判断当前用户是否能查看该版块内容
zib_bbs_get_posts_allow_view_btn()在帖子列表、帖子头部等位置显示权限徽章
zib_bbs_get_plate_allow_view_btn()在版块相关位置显示权限徽章

判断顺序非常关键。帖子是否可见不是只看帖子自身:

$plate_id   = zib_bbs_get_plate_id($post->ID);
$plate_data = zib_bbs_get_allow_view_data(get_post($plate_id));

if ($plate_data['open'] && !$plate_data['allow_reason']) {
    return $plate_data['not_html'];
}

$data = zib_bbs_get_allow_view_data($post);

也就是说,版块限制优先于帖子限制。给版块设置查看限制后,版块内所有帖子列表、帖子页内容都会先受版块限制影响;只有版块允许查看后,才会继续判断帖子自己的阅读限制。

支持的 allow_view 类型:

含义关键判断
空值公开不启用限制
signin登录后可查看未登录显示登录/注册入口
comment评论后可查看zib_user_is_commented() 判断当前用户是否已评论
roles部分用户可查看读取 allow_view_roles,支持会员、等级、认证用户
pay付费查看读取 posts_zibpay,通过 zibpay_is_paid() 判断购买状态
points支付积分查看同样走 posts_zibpaypay_modo=points
follow关注版块后可查看用于版块查看限制,调用 zib_bbs_is_followed_plate()

管理员、论坛管理员、内容作者、版主或分区版主会在 zib_bbs_get_allow_view_data() 里得到 allow_reason,即使内容设置了限制也可以查看。扩展权限时不要绕过这个函数,否则很容易让版主、作者、购买用户或会员用户出现前台显示和后台权限不一致。

帖子阅读权限保存入口是 edit_allow_view

add_action('wp_ajax_edit_allow_view', 'zib_bbs_ajax_edit_allow_view');

它会校验:

场景能力
发布帖子时设置阅读权限posts_allow_view_add
修改已发布帖子的阅读权限posts_allow_view_edit
设置积分可见posts_allow_view_points
设置付费可见posts_allow_view_pay

保存的数据:

Meta内容
allow_view阅读限制类型
allow_view_roles会员、等级、认证用户等允许查看条件
posts_zibpay付费/积分价格、会员价、推广折扣、购买说明
pay_hide_part是否只隐藏部分内容

版块查看权限保存入口是 plate_edit_allow_view,保存 allow_viewallow_view_rolesplate 的 post meta。如果版块 allow_view 选择 follow,保存时还会同步执行 zib_bbs_ajax_plate_edit_follow_pay(),因为关注后可查看通常会和付费关注一起使用。

付费版块与付费关注

付费版块不是单独的 post type,而是版块 plate 的关注能力叠加支付参数。核心数据由 zib_bbs_get_plate_follow_pay_options($plate_id) 读取:

Meta说明
follow_pay是否开启付费关注
follow_pay_args支付方式、现金价格、积分价格、会员价等参数

默认数据结构:

$data = array(
    's'            => (int) get_post_meta($plate_id, 'follow_pay', true),
    'pay_modo'     => '0',
    'pay_price'    => 0,
    'vip_1_price'  => 0,
    'vip_2_price'  => 0,
    'points_price' => 0,
    'vip_1_points' => 0,
    'vip_2_points' => 0,
);

前台付费关注流程由 zib_bbs_get_plate_follow_pay_cashier_modal() 输出收银台。它会先查当前用户是否已有待支付订单:

$wait_payment_data = zibpay_get_current_user_post_wait_payment($plate_id, 11);

没有待支付订单时再根据 pay_modo 生成积分支付或现金支付表单。现金支付会走 zibpay_get_initiate_pay_input(),订单类型是 11;积分支付会检查当前用户积分是否足够,不足时给出积分入口。

付费关注保存入口是:

add_action('wp_ajax_plate_edit_follow_pay', 'zib_bbs_ajax_plate_edit_follow_pay');

保存时会做价格校验:

  • 现金价格必须大于 0,并受 bbs_plate_follow_pay_opt.price_limit.price 限制。
  • 积分价格必须大于 0,并受 bbs_plate_follow_pay_opt.price_limit.points 限制。
  • 会员价不能高于普通价。
  • 二级会员价不能高于一级会员价。
  • 未单独填写会员价时,按后台折扣自动计算。

用户点击关注时走 follow_plate;如果版块开启了付费关注,未支付用户会进入收银台。免费确认和取消确认会走 follow_plate_confirm。因此扩展付费版块时,不要自己写独立订单表或独立关注表,应接入现有 follow_platesubmit_orderfollow_plate_confirm 流程。

如果要在付费关注成功后同步业务记录,可以挂已有的关注 Hook:

add_action('bbs_follow_plate', 'zib_docs_bbs_follow_plate_log', 10, 2);

function zib_docs_bbs_follow_plate_log($plate_id, $user_id)
{
    $plate_id = (int) $plate_id;
    $user_id  = (int) $user_id;

    if (!$plate_id || !$user_id) {
        return;
    }

    update_post_meta($plate_id, '_docs_last_follow_time', current_time('mysql'));
}

发布限制与创建版块限制

论坛发布限制分两层:

类型存储位置控制能力
在某个版块发帖plateadd_limit post metaselect_plateposts_add
在某个版块分类创建版块plate_catadd_limit term metaselect_plate_catplate_add

后台可以配置限制选项数量:

配置说明
bbs_posts_add_limit_opt_max发帖限制选项数量
bbs_plate_add_limit_opt_max创建版块限制选项数量
user_cap.bbs_posts_add_limit_{N}第 N 个发帖限制选项对应的用户组
user_cap.bbs_plate_add_limit_{N}第 N 个创建版块限制选项对应的用户组

选项生成入口是:

zib_bbs_get_add_limit_options($type);

权限判断集中在 zib_bbs_user_can()。发帖时先判断基础 posts_add,再判断是否有选择目标版块的权限:

if (!zib_bbs_current_user_can('select_plate', $plate, $post_id)) {
    zib_send_json_error(sprintf(__('您暂无在此%s发布的权限,请重新选择%s', 'zib_language'), $zib_bbs->plate_name, $zib_bbs->plate_name));
}

创建版块时先判断基础 plate_add,再判断是否有选择目标版块分类的权限:

if (!zib_bbs_current_user_can('select_plate_cat', $cat, $plate_id)) {
    zib_send_json_error(sprintf(__('您没有在此分类创建%s的权限,请重新选择%s分类', 'zib_language'), $name, $name));
}

发布限制的弹窗和保存入口:

Action回调说明
set_add_limit_modalzib_bbs_ajax_set_add_limit_modal输出发布限制设置弹窗
save_add_limitzib_bbs_ajax_save_add_limit保存 add_limit 到 post meta 或 term meta

版块编辑表单也会内联这些设置:zib_bbs_edit::input_add_limit('posts', $plate_id) 用于设置某个版块的发帖限制;版块分类保存时,ajax-term.php 会把 add_limit 写入 term meta。

扩展发布限制时有两个原则:

  1. 新增限制条件要进入 zib_bbs_get_add_limit_options()zib_bbs_user_can() 的能力链路,而不是只在前端隐藏发布按钮。
  2. 前台展示权限提示时复用 zib_bbs_get_add_limit_btn()zib_bbs_get_plate_badge_popover(),这样版块卡片、版块头部和侧栏小工具里的权限徽章会保持一致。

后台列表与批量维护

论坛后台维护分两层:inc/functions/bbs/inc/class.init.php 负责后台查询、筛选、回收站和评论列表入口;inc/functions/bbs/admin/meta-option.php 负责帖子后台 meta box、快速编辑和批量编辑字段。

能力入口源码位置说明
后台帖子按版块筛选restrict_manage_postszib_bbs::plate_select()只在 forum_post 列表输出版块下拉
后台主查询按版块过滤pre_get_postszib_bbs::admin_post_query()读取 $_GET['plate_id'],追加 plate_id meta 查询
帖子快速/批量编辑字段bulk_edit_custom_boxquick_edit_custom_boxzib_bbs_posts_bulk_edit::edit_box()只在 post_type=forum_post 时挂载
快速/批量编辑保存save_postzib_bbs_posts_bulk_edit::save()只处理 screen=edit-forum_postzib_bulk_edit['forum_post'] 存在的请求
版块移入回收站trashed_postzib_bbs::trashed_plate()版块移入回收站时,把所属已发布帖子直接批量改为 trash
从回收站恢复wp_untrash_post_statuszib_bbs::untrash_post_status()plateforum_post 恢复时直接设为 publish
用户列表统计manage_users_columnsmanage_users_custom_columnzib_bbs::users_columns()zib_bbs::users_custom_column()在用户列表显示版块数和帖子数,并链接到对应后台列表

后台快速/批量编辑目前会输出这些字段:

字段保存行为
plate_id更新帖子所属版块
topping更新置顶级别
bbs_type更新帖子类型
essence更新精华标记
views支持设置、增加、减少、乘除,最终不低于 0
allow_view更新帖子查看权限

生效的批量编辑类是 zib_bbs_posts_bulk_editinc/functions/bbs/inc/class.init.php 里也保留了 bulk_edit_custom_box()bulk_save_post() 方法,但当前 setup() 没有挂载这两个方法;二次开发时不要误以为两套批量编辑都会执行。

后台批量维护和前台 Ajax 的边界不同。比如前台移动版块会调用 zib_bbs_posts_plate_move(),它会刷新新旧版块统计并触发 posts_plate_move;后台快速/批量编辑直接更新 plate_id,随后 save_post 统计刷新只会按当前新 plate_id 处理,旧版块统计和监听 posts_plate_move 的扩展可能不会同步。

因此批量迁移帖子所属版块时,更稳的做法是自己调用主题移动函数:

function zib_docs_bbs_batch_move_posts($post_ids, $new_plate_id)
{
    $new_plate_id = (int) $new_plate_id;
    if (!$new_plate_id || get_post_type($new_plate_id) !== 'plate') {
        return;
    }

    foreach ((array) $post_ids as $post_id) {
        $post_id = (int) $post_id;
        if (!$post_id || get_post_type($post_id) !== 'forum_post') {
            continue;
        }

        $old_plate_id = zib_bbs_get_plate_id($post_id);
        if (!$old_plate_id || $old_plate_id === $new_plate_id) {
            continue;
        }

        zib_bbs_posts_plate_move($post_id, $new_plate_id, $old_plate_id);
    }
}

如果必须使用后台批量编辑改 plate_id,保存后要刷新新旧版块统计、动态推荐缓存和自己的外部索引。不能只看帖子 meta 已经变了,就认为版块页、小工具、推荐排序和消息监听都已经同步。

版块移入回收站也要谨慎。trashed_plate() 用 SQL 直接把该版块下已发布帖子改为 trash,这不会逐篇触发前台删除 Ajax 的通知、原因记录或自定义 Hook。需要给作者发消息、写审计日志或迁移帖子时,优先走前台 plate_delete 流程里的“迁移到新版块”逻辑,或在批量任务里显式补齐通知和缓存刷新。

后台用户列表会追加一列:

$columns['bbs_count'] = $this->plate_name . ' · ' . $this->posts_name;

列内容分别链接到:

edit.php?post_type=plate&author={user_id}
edit.php?post_type=forum_post&author={user_id}

如果要扩展用户列表统计,优先追加新列或过滤这一列输出,不要把耗时统计塞进每个用户行。用户列表可能一次展示大量用户,复杂统计应使用已有计数函数或缓存。

源码里还提供了后台评论列表和版块/帖子列表列函数:

函数作用当前注意点
views_comments()在评论列表状态链接中增加论坛评论和文章评论切换comment_status_links 挂载在当前源码中是注释状态
comments_list_table_query_args()评论列表默认不显示论坛帖子评论当前源码中可见函数,但要确认实际挂载再依赖
plate_columns() / plate_custom_column()版块后台列表显示版主、统计、最后发布时间当前 setup() 中未看到对应 manage_*_columns 挂载
posts_columns() / posts_custom_column()帖子后台列表显示所属版块、阅读、评分、收藏当前 setup() 中未看到对应 manage_*_columns 挂载
plate_cat_columns() / plate_cat_custom_column()版块分类列表显示分区版主当前 setup() 中未看到对应 taxonomy 列挂载

二次开发时不要只看到函数存在就认为后台界面已经启用。先搜索是否挂到了 WordPress 的 manage_*_columnsmanage_*_custom_columncomment_status_linkscomments_list_table_query_args,再决定是复用、补挂载,还是另写自己的后台列。

帖子视频、图集与图片封面

论坛帖子封面使用与文章封面相同的核心 meta key,但保存和前台渲染在论坛模块里单独接管。优先级固定为:

featured_video > featured_slide > cover_image

后台编辑页的封面 meta box 来自 inc/functions/bbs/admin/meta-option.php

字段类型说明
featured_videouploadlibrary=video视频封面地址
featured_slidegallery图集/幻灯片封面,保存附件 ID 列表
cover_imageuploadlibrary=image图片封面,也作为视频首图封面

后台保存入口是 save_post 上的 zib_bbs_save_meta_box_forum_cover(),它只处理这三个字段:

$meta_fields = array('featured_video', 'featured_slide', 'cover_image');
foreach ($meta_fields as $field) {
    if (isset($_POST[$field])) {
        zib_update_post_meta($post_id, $field, $_POST[$field]);
    }
}

前台发帖/编辑页使用 zib_bbs_edit::posts_featured() 输出封面编辑器。它先判断图片封面权限,图片封面没有权限时整个封面编辑器不会输出;然后再按能力打开图集和视频选项:

能力说明
posts_image_cover允许设置图片封面,也是封面编辑器的基础权限
posts_slide_cover允许设置幻灯片/图集封面
posts_video_cover允许设置视频封面

这些能力在后台论坛权限里定义为 bbs_posts_image_coverbbs_posts_slide_coverbbs_posts_video_coverposts_slide_coverposts_video_cover 都依赖图片封面能力,因为视频首图、图集预览和普通图片封面共享上传入口与比例配置。

前台 Ajax 保存入口是 zib_bbs_ajax_edit_posts_featured(),它读取 featured_data.type

featured_data.type保存行为
video写入 featured_video;如果传入 pic,同时写入 cover_image
slide清空 featured_video,把附件 ID 数组写入 featured_slide
image清空 featured_videofeatured_slide,写入 cover_image
close清空 featured_videofeatured_slidecover_image

这个清理顺序是为了保证前台渲染优先级稳定。比如切换到普通图片封面时,必须清空视频和图集,否则列表页仍会优先显示旧视频或旧图集。

封面渲染链路

列表页详细样式会先判断帖子是否允许查看:

$cover = zib_bbs_posts_is_can_viewed($post) ? zib_bbs_get_posts_lists_cover($post) : '';

因此被阅读限制挡住的帖子不会在列表中暴露真实封面。允许查看时,zib_bbs_get_posts_lists_cover() 按顺序渲染:

  1. 如果 bbs_posts_cover_opt.lists_video_s 开启,并且有 featured_video,输出 .forum-thumb-video
  2. 否则尝试 zib_bbs_get_posts_slide_cover($post->ID)
  3. 否则尝试 zib_bbs_get_posts_image_cover($post->ID)

视频列表封面不会直接输出完整播放器,而是输出视频缩略容器:

$cover = '<div class="forum-thumb-video" style="--scale-height:' . $scale_height . '%;">'
    . '<div class="video-thumb-box" video-url="' . esc_url($video) . '"' . $mute_attr . '>'
    . '<div class="img-thumb">' . $pic_html . '</div><div class="video-thumb"></div>'
    . '</div></div>';

这里的 video-urldata-volume 交给论坛前台脚本初始化。lists_video_mute_s 开启时输出 data-volume="none",否则输出 data-volume="100"。如果帖子没有设置 cover_image 作为视频首图,zib_bbs_get_video_cover_pic() 会从 bbs_posts_cover_opt.video_spare_pic 随机取一张备用图。

详情页封面由 zib_bbs_get_single_cover() 输出,位置在帖子页内容区顶部、面包屑之前:

do_action('bbs_posts_page_content_top');
echo zib_bbs_get_single_cover();
if (apply_filters('single_show_breadcrumbs', true)) {
    echo zib_bbs_get_breadcrumbs();
}

详情页同样先检查 zib_bbs_posts_is_can_viewed($post)。可见时,视频封面会直接调用 zib_get_dplayer($video, $pic_url, $scale_height) 输出播放器;图集和图片封面会传入 $show_breadcrumbs=true,把面包屑叠到封面左下角,并通过 add_filter('single_show_breadcrumbs', '__return_false') 阻止页面再输出一遍面包屑。

封面比例和列表视频行为都来自 bbs_posts_cover_opt

配置用途
image_ratio图片封面比例
slide_ratio幻灯片封面比例
video_ratio视频封面比例
lists_video_s列表页是否显示视频封面
lists_video_mute_s列表视频预览是否静音
video_spare_pic未设置视频首图时的备用封面图库

扩展封面时要复用这些入口。比如给帖子自动补一张封面图,可以在发帖保存后的业务 Hook 里写入 cover_image,不要另起 meta key:

add_action('bbs_add_posts', 'zib_docs_bbs_posts_default_cover');

function zib_docs_bbs_posts_default_cover($post)
{
    if (empty($post->ID) || get_post_type($post->ID) !== 'forum_post') {
        return;
    }

    $cover = zib_get_post_meta($post->ID, 'cover_image', true);
    if ($cover) {
        return;
    }

    $default_cover = apply_filters('zib_docs_bbs_default_cover_url', '', $post);
    if (!$default_cover) {
        return;
    }

    zib_update_post_meta($post->ID, 'cover_image', esc_url_raw($default_cover));
}

如果要改变封面显示顺序,应优先包装 zib_bbs_get_posts_lists_cover()zib_bbs_get_single_cover() 的调用位置,而不是改 featured_videofeatured_slidecover_image 的语义。主题列表、详情页和通用封面逻辑都会默认这三个 key 的含义不变。

前台管理操作

帖子详情页的管理菜单来自 inc/functions/bbs/inc/single.phpinc/functions/bbs/inc/posts.php。主题不会把“能看见按钮”当成权限本身,每个 Ajax 回调仍会再次校验 nonce、目标对象和 zib_bbs_current_user_can()。二次开发时应复用这些链接生成函数,而不是手写按钮直连 Ajax。

操作前台入口Ajax Action权限能力数据变化
设置精华zib_bbs_get_posts_essence_set_link()posts_essence_setposts_essence_set写入 essence meta
设置置顶zib_bbs_get_posts_topping_set_link()posts_topping_set_modal / posts_topping_setposts_topping_set写入 topping meta
移动版块zib_bbs_get_posts_plate_move_link()posts_plate_move_modal / plate_moveposts_plate_move + select_plate更新 plate_id meta
审核/驳回zib_bbs_get_posts_audit_link()posts_audit_modal / posts_auditposts_audit切换 post_status
关闭/开启评论详情页管理菜单posts_comment_close_modal / posts_comment_closecomment_close切换 comment_status
采纳回答zib_bbs_get_comment_adopt_link()answer_adopt_modal / answer_adoptquestion_answer_adopt写入 question_status 和评论 adopted
设置热评评论管理菜单comment_set_hotcomment_set_hot写入评论 is_hot

精华和置顶共用 zib_bbs_ajax_posts_meta_save()。它会用当前 action 直接做能力名,因此新增类似管理动作时要避免让未受控的 action 进入通用 meta 保存函数:

if (!zib_bbs_current_user_can($action, $post_id)) {
    echo json_encode(array(
        'error' => 1,
        'ys'    => 'danger',
        'msg'   => __('暂无此权限', 'zib_language'),
    ));
    exit;
}

置顶不是布尔值。topping 会保存为具体置顶级别,展示文案来自 zib_bbs_get_posts_topping_options();取消置顶时保存 0。精华则使用 essence=1 或空值。两个操作分别触发:

do_action('bbs_posts_essence_set', $post_id, $val);
do_action('bbs_posts_topping_set', $post_id, $topping);

移动版块要校验两层权限:先确认当前用户能移动这个帖子,再确认用户能选择目标版块。真正更新由 zib_bbs_posts_plate_move() 完成,它不只改 plate_id,还会触发新旧版块的 save_post,并触发 posts_plate_move

function zib_bbs_posts_plate_move($id, $new_id, $old_id)
{
    if ($new_id == $old_id) {
        return;
    }

    update_post_meta($id, 'plate_id', $new_id);
    do_action('save_post', $new_id, get_post($new_id), true);
    do_action('save_post', $old_id, get_post($old_id), true);
    do_action('posts_plate_move', $id, $new_id, $old_id);
}

因此批量移动或后台自定义移动时,也应调用 zib_bbs_posts_plate_move(),不要只写 plate_id。否则版块统计、推荐分缓存和监听 posts_plate_move 的扩展都不会同步。

审核入口 zib_bbs_ajax_plate_or_posts_audit() 同时服务版块和帖子。pending 状态下默认发布;如果传入 method=reject,会要求填写驳回原因,保持 pending 并调用 zib_newmsg_publish_to_pending($post) 通知作者。非 pending 内容再次操作会转回 pending

if ('pending' === $post->post_status) {
    if ('reject' === $method) {
        $post_status = 'pending';
        zib_newmsg_publish_to_pending($post);
    } else {
        $post_status = 'publish';
    }
} else {
    $post_status = 'pending';
}

采纳回答只允许已审核评论,并且会防重复。真正落库在 zib_bbs_answer_adopt():帖子写入 question_status=1,评论写入 adopted=1,随后触发 answer_adopted。主题消息模块会监听这个 Hook,只通知一次,并按 email_bbs_answer_adopted 决定是否发邮件。

add_action('answer_adopted', 'zib_docs_bbs_answer_adopted_log', 20, 2);

function zib_docs_bbs_answer_adopted_log($comment, $desc = '')
{
    if (empty($comment->comment_ID)) {
        return;
    }

    update_comment_meta($comment->comment_ID, 'docs_adopted_time', current_time('mysql'));
}

热评既可以由管理员手动设置,也可以由点赞逻辑自动触发。手动 comment_set_hot 会先删除 posts_lists_hot_comment 缓存,再写评论 meta;设置为热评时触发 comment_is_hot,取消热评不会触发通知 Hook。扩展通知或奖励时要监听 comment_is_hot,并自行做好只奖励一次的标记。

版主申请与管理

论坛有三类管理身份:分区版主、版块创建者和版块版主。它们不是 WordPress role,而是主题权限体系里的动态身份:

身份判断函数数据来源
分区版主zib_bbs_is_the_cat_moderator()plate_cat term meta:moderator
版块创建者zib_bbs_is_the_moderator() 返回 plate_authorplatepost_author
版块版主zib_bbs_is_the_moderator() 返回 moderatorplate post meta:moderator

后台权限项允许把能力授予这些动态身份。比如版主相关权限来自“论坛[用户权限]”:

能力说明默认倾向
apply_moderator普通用户申请成为版主排除已是版主、版块创建者、分区版主的用户
moderator_apply_process处理、审核版主申请默认给版块创建者和分区版主
moderator_add为管理的版块添加版主默认给版块创建者和分区版主
moderator_edit删除、修改版主默认给版块创建者和分区版主

能力判断仍然走 zib_bbs_current_user_can()moderator_addmoderator_editmoderator_apply_process 这类能力只对“自己管理下的对象”生效,会继续检查当前用户是否是目标版块的版主、版块创建者或分区版主。

版主身份的写入统一走 zib_bbs_update_moderator(),不要直接改 meta:

function zib_bbs_update_moderator($action = 'add', $type = 'plate', $id = 0, $user_id = 0)
{
    if (!$id || !$user_id) {
        return false;
    }

    if ('cat' === $type) {
        $moderator = get_term_meta($id, 'moderator', true);
    } else {
        $moderator = get_post_meta($id, 'moderator', true);
    }

    if (!$moderator || !is_array($moderator)) {
        $moderator = array();
    }

    if ('delete' === $action) {
        if (in_array($user_id, $moderator)) {
            $index = array_search($user_id, $moderator);
            unset($moderator[$index]);
        }
    } else {
        if (!in_array($user_id, $moderator)) {
            $moderator[] = (string) $user_id;
        }
    }

    if ('cat' === $type) {
        update_term_meta($id, 'moderator', $moderator);
        wp_cache_delete($user_id, 'cat_moderator_ids');
        zib_bbs_get_user_cat_moderator_ids($user_id);
    } else {
        update_post_meta($id, 'moderator', $moderator);
        wp_cache_delete($user_id, 'plate_moderator_ids');
        zib_bbs_get_user_plate_moderator_ids($user_id);
    }

    return true;
}

新增版主和移出版主的 Ajax 都在 inc/functions/bbs/action/ajax-user.php。流程是:打开弹窗、搜索用户、确认用户、提交写入。提交时会再次校验 nonce、目标对象和权限,然后调用 zib_bbs_moderator_add_user()zib_bbs_moderator_remove_user()。这两个函数会先写入/删除 moderator meta,再给目标用户发送站内消息,必要时发送邮件。

Action用途关键校验
moderator_modal查看版主列表可匿名查看
moderator_edit_modal管理版主列表moderator_editcat_moderator_edit
moderator_add_modal打开添加版主弹窗moderator_addcat_moderator_add
moderator_add_search搜索可添加用户搜索结果还要在提交时复核权限
moderator_add_user添加版主nonce、目标对象、moderator_add
moderator_remove移出版主nonce、目标对象、moderator_edit

申请成为版主不是直接写入 moderator。用户提交 apply_moderator 时,主题会先校验 apply_moderator 能力、apply_moderator nonce 和申请说明长度,然后创建一条 type=moderator_applystatus=0 的站内消息:

$msg_args = array(
    'send_user'    => $user_id,
    'receive_user' => 'admin',
    'type'         => 'moderator_apply',
    'title'        => $title,
    'content'      => $msg_con . $motel_btn,
    'meta'         => array(
        'plate_id' => $plate->ID,
        'desc'     => $desc,
        'time'     => current_time('Y-m-d H:i:s'),
    ),
);

ZibMsg::add($msg_args);

待处理申请通过 zib_bbs_get_apply_moderator_processing() 查询,本质是查当前用户发出的 moderator_apply 未处理消息。审批时,apply_moderator_process 只接受 process=1process=2:通过时调用 zib_bbs_add_moderator('plate', $plate_id, $user_id),拒绝时只更新消息状态并给用户发回复消息。

if (1 == $process) {
    zib_bbs_add_moderator('plate', $plate_id, $user_id);
}

扩展版主申请时要保留这个“两段式”流程。比如要记录审批结果,可以监听申请回复消息或监听 moderator meta 变化,不要在申请提交阶段提前把用户加入版主数组:

add_action('updated_post_meta', 'zib_docs_bbs_moderator_meta_log', 20, 4);

function zib_docs_bbs_moderator_meta_log($meta_id, $post_id, $meta_key, $meta_value)
{
    if ('moderator' !== $meta_key || get_post_type($post_id) !== 'plate') {
        return;
    }

    update_post_meta($post_id, 'docs_moderator_updated_at', current_time('mysql'));
}

版主缓存要特别小心。主题会缓存用户管理的分区、版块创建者列表和版块版主列表,缓存组分别是 cat_moderator_idsplate_author_idsplate_moderator_ids。使用主题函数添加或移出版主时会自动刷新;如果外部脚本批量改 moderator meta,必须同步删除对应用户缓存。

用户中心入口

论坛会把版块、帖子、收藏和论坛身份接入作者页。入口集中在 inc/functions/bbs/inc/user-page.php,列表查询集中在 inc/functions/bbs/inc/user.php,分页 Ajax 在 inc/functions/bbs/action/ajax-user.php

入口Hook / Action说明
用户主页版块 Tabauthor_main_tab_plate标题显示用户创建版块数;如果没有创建版块,会显示用户管理版块数
用户主页帖子 Tabauthor_main_tab_forum标题显示用户发布的论坛帖子数,并启用 route
用户主页版块内容main_author_tab_content_plate输出状态筛选、排序和版块列表
用户主页帖子内容main_author_tab_content_forum输出状态筛选、排序和帖子列表
收藏列表里的论坛帖子author_favorite_lists_forum_post在用户收藏页显示收藏的论坛帖子
收藏类型扩展author_favorite_types给收藏页追加 forum_post 类型和排序项
作者头部身份author_header_identity追加论坛身份徽章
异步版块分页Ajax author_plate调用 zib_bbs_get_user_plate_lists()
异步帖子分页Ajax author_forum_posts调用 zib_bbs_get_user_posts_lists()

用户主页版块 Tab 不是只看作者创建的 plate。它会先取 zib_bbs_get_user_plate_count($author_id),如果为 0,再取 zib_bbs_get_user_moderator_plate_count($author_id)。因此用户没有创建版块、但正在管理版块时,也应该能在作者页看到版块入口。

版块列表支持三种状态:

状态查询含义数据来源
publish用户创建的公开版块plate post author
moderator用户管理的版块moderator post meta 中包含用户 ID
follow用户关注的版块user meta follow_plate

帖子列表默认显示用户发布的 forum_post。当前登录用户查看自己的主页,或超级管理员查看用户主页时,才会看到 pendingdraft;其他访客始终只能看到 publish。扩展用户主页时不要把待审核或草稿入口公开到所有访客。

列表排序项
版块datelast_postlast_replyposts_countreply_countviewsfollow_count
帖子datemodifiedlast_replyviewsscorecomment_countfavorite_count
收藏post__indatemodifiedviewscomment_countfavorite_count

版块关注数缓存组是 user_favorite_plate_count,帖子收藏数缓存组是 user_favorite_posts_count。主题分别监听 bbs_follow_platebbs_favorite_posts 删除缓存并重新计算。扩展关注或收藏时要触发这两个 Hook,不要只改 user meta。

示例:给作者页论坛帖子 Tab 追加一个自定义提示,同时保留主题的 route 和 loader:

add_filter('author_main_tab_forum', 'zib_docs_author_main_tab_forum_note', 20, 2);

function zib_docs_author_main_tab_forum_note($tab, $author_id)
{
    if (empty($tab['title'])) {
        return $tab;
    }

    $tab['title'] .= '<span class="badg badg-sm ml6 c-blue">' . __('动态', 'zib_language') . '</span>';
    return $tab;
}

Ajax 动作地图

论坛前台动作集中在 inc/functions/bbs/action/*。常用动作:

Action回调说明
author_platezib_bbs_ajax_user_plate_lists用户主页版块分页
author_forum_postszib_bbs_ajax_user_posts_lists用户主页论坛帖子分页
bbs_posts_save / bbs_posts_draftzib_bbs_ajax_edit_posts发布、编辑、保存草稿
score_extra / score_deductzib_bbs_ajax_posts_score给帖子加分或扣分
favorite_postszib_bbs_ajax_favorite_posts收藏或取消收藏帖子
posts_essence_setzib_bbs_ajax_posts_meta_save设置精华
posts_topping_setzib_bbs_ajax_posts_meta_save设置置顶
plate_movezib_bbs_ajax_posts_plate_move移动帖子所属版块
posts_deletezib_bbs_ajax_plate_or_posts_delete删除论坛帖子到回收站
plate_deletezib_bbs_ajax_plate_or_posts_delete删除版块,可选择迁移版块下帖子
posts_delete_revoke / plate_delete_revokezib_bbs_ajax_plate_or_posts_delete_revoke从回收站恢复帖子或版块
answer_adoptzib_bbs_ajax_answer_adopt采纳回答
comment_set_hotzib_bbs_ajax_comment_set_hot设置热评
save_platezib_bbs_ajax_save_plate创建或编辑版块
follow_platezib_bbs_ajax_follow_plate关注版块
save_plate_catzib_bbs_ajax_save_term保存版块分类
save_forum_topiczib_bbs_ajax_save_term保存话题
save_forum_tagzib_bbs_ajax_save_term保存标签
apply_moderatorzib_bbs_ajax_apply_moderator申请版主
apply_moderator_processzib_bbs_ajax_apply_moderator_process处理版主申请
moderator_add_userzib_bbs_ajax_moderator_add_user添加版主
moderator_removezib_bbs_ajax_moderator_remove移除版主

这些动作会校验登录态、目标对象、权限、版主身份、作者身份、状态和 nonce。新增论坛 Ajax 时不要只判断 is_user_logged_in()

关键业务 Hook

帖子发布和编辑会触发:

do_action('zib_pre_insert_post', $insert_args);
do_action('bbs_' . ($is_new ? 'add' : 'edit') . '_posts', $new_post_obj);

评分、收藏、置顶、精华和版块关注会触发:

do_action('bbs_user_' . $action, $author_id, $id, $author_score);
do_action('bbs_' . $action, $id, $user_id, $new_user_score);
do_action('bbs_favorite_posts', $id, $user_id, $type);
do_action('bbs_posts_essence_set', $post_id, $val);
do_action('bbs_posts_topping_set', $post_id, $topping);
do_action('bbs_follow_plate', $id, $user_id);

回答采纳和热评会触发:

do_action('answer_adopted', $comment, $desc);
do_action('comment_is_hot', $comment);

这些 Hook 不只服务论坛本身。消息通知、等级经验、徽章统计和部分用户成长逻辑都会监听它们。比如 bbs_score_extrabbs_posts_essence_setposts_is_hotplate_is_hotcomment_is_hotanswer_adopted 都会影响用户成长,详见 用户成长、权限与徽章

示例:在帖子加精后同步站内消息或日志:

add_action('bbs_posts_essence_set', 'zib_docs_bbs_posts_essence_log', 10, 2);

function zib_docs_bbs_posts_essence_log($post_id, $val)
{
    $post_id = (int) $post_id;
    if (!$post_id || get_post_type($post_id) !== 'forum_post') {
        return;
    }

    update_post_meta($post_id, '_docs_last_essence_time', current_time('mysql'));
}

消息通知

论坛通知不是一个独立弹窗逻辑,而是挂在论坛业务 Hook 上,再交给主题消息系统、邮件开关和用户接收设置处理。扩展时应该监听同一批 Hook,保留防重复 meta,避免在前端按钮点击时直接写消息。

触发 Hook核心回调消息场景防重复字段邮件开关
answer_adoptedzib_bbs_answer_adopted_msg()回答被采纳,通知回答用户comment meta is_adopted_notify_pz('email_bbs_answer_adopted', true)
comment_is_hotzib_bbs_comment_is_hot_msg()评论成为热评,通知评论用户comment meta is_hot_notify跟随站内消息接收设置
posts_is_hotzib_bbs_new_msg_is_hot()帖子成为热门,通知帖子作者post meta is_hot_notify跟随站内消息接收设置
plate_is_hotzib_bbs_new_msg_is_hot()版块成为热门,通知版块作者post meta is_hot_notify跟随站内消息接收设置
bbs_posts_essence_setzib_bbs_posts_essence_msg()帖子被设为精华,通知帖子作者由业务状态控制,取消精华不通知跟随站内消息接收设置
bbs_add_postszib_bbs_add_posts_pending_msg()新帖进入待审核,通知管理员、版块作者和版主post meta add_posts_pending_msg_pz('email_bbs_posts_pending_to_admin', true)_pz('email_bbs_posts_pending_to_moderator', true)

这些回调会先判断作者、评论用户或版主身份,再通过 zib_msg_is_allow_receive() 判断用户是否允许接收对应类型的消息。待审核通知还受 _pz('msg_include_plate') 控制;如果站点关闭论坛待审消息,不应该在扩展里强行绕过。

zib_bbs_add_posts_publish_msg() 在源码中存在,但 bbs_add_posts 上的发布通知版主挂载被注释,说明默认流程只保留待审核通知。二次开发如果需要“新帖发布通知版主”,可以复用 bbs_add_posts,但要自己补开关和防重复字段。

示例:监听热门帖子事件并写入一次性记录:

add_action('posts_is_hot', 'zib_docs_bbs_hot_posts_record');

function zib_docs_bbs_hot_posts_record($post)
{
    $post = get_post($post);
    if (empty($post->ID) || 'forum_post' !== $post->post_type) {
        return;
    }

    if (zib_get_post_meta($post->ID, '_docs_hot_recorded')) {
        return;
    }

    zib_update_post_meta($post->ID, '_docs_hot_recorded', array(
        'time'  => current_time('mysql'),
        'views' => (int) get_post_meta($post->ID, 'views', true),
    ));
}

权限与能力边界

论坛会接入主题权限体系:

add_filter('is_can_roles', 'zib_bbs_is_can_roles_filter', 10, 3);
add_filter('zib_user_can', 'zib_bbs_user_can_filter', 10, 4);
add_filter('hascaps_roles_lists', 'zib_bbs_hascaps_roles_lists_filter', 10, 3);

扩展时要分清:

身份能力边界
普通用户发帖、收藏、评分、关注、申请版主,受后台开关和版块限制影响
帖子作者编辑自己的帖子,是否允许删除、关闭评论、修改权限取决于配置
版主管理所属版块内的帖子、评论、申请和部分设置
管理员全局管理版块、话题、标签、帖子状态和后台字段

付费查看和论坛权限有交叉。主题会通过 sanitize_post_meta_posts_zibpay_for_forum_post 修正论坛帖子付费字段,并通过 tourists_pay_is_allow 禁止论坛帖子免密购买。

版块统计与缓存

论坛统计会同时写入 post meta、term meta、对象缓存和全局查询结果。列表页、小工具、版块页头部和热门判断都依赖这些结果,直接改 meta 很容易让页面显示和排序状态不一致。

数据写入位置刷新时机缓存组
版块帖子数 posts_count版块 post metasave_post 保存论坛帖子或版块时,经 zib_bbs_updata_plate_posts_count() 刷新plate_posts_count_allplate_posts_count_today
版块回复数 reply_counttoday_reply_count版块 post metasave_postwp_update_comment_countzib_bbs_updata_plate_reply_count() 刷新plate_reply_count_allplate_reply_count_today
版块阅读数 views版块 post metaposts_views_recordzib_bbs_updata_views() 汇总版块下所有帖子阅读数plate_views_count
分区阅读数 viewsplate_cat term metaposts_views_record 汇总分区下版块阅读数term_views_count
最近发帖 last_post版块 post meta、plate_cat term meta已发布论坛帖子保存时写入无固定对象缓存,依赖 meta
最近回复 last_reply帖子 post meta、版块 post meta、plate_cat term metacomment_postcomment_unapproved_to_approved 审核通过时写入无固定对象缓存,依赖 meta
热门状态 is_hot帖子或版块 post metaposts_views_recordzib_bbs_updata_is_hot() 判断bbs_is_hot

帖子数通过 zib_bbs_get_plate_posts_count($plate_id, $today) 读取。函数会先读 plate_posts_count_allplate_posts_count_today,再用一次分组查询缓存所有版块结果;今日统计会按 post_date_gmt 限定当天。

回复数通过 zib_bbs_get_plate_reply_count($plate_id, $today) 读取。函数会统计已审核评论,并且只计算已发布的 forum_post。今日回复数按 comment_date_gmt 限定当天,因此跨时区展示时要优先相信主题函数返回值,不要自己用本地时间拼 SQL。

热门帖子判断会读取帖子 viewsscorecomment_count,再和版块平均阅读量比较;过滤器是 bbs_is_hot_posts。热门版块判断会读取版块帖子数、阅读数、回复数,再和分区平均阅读量比较;过滤器是 bbs_is_hot_plate。当 is_hot 从旧值变为新值且结果为真时,才会触发 posts_is_hotplate_is_hot

删除、移动和恢复也属于统计维护链路:

场景主题函数 / Action维护要点
单帖移动版块zib_bbs_posts_plate_move($id, $new_id, $old_id)写入帖子 plate_id,对新旧版块分别触发 save_post,再触发 posts_plate_move
版块删除前迁移帖子zib_bbs_plates_move($new_id, $old_id)批量更新帖子 plate_id,触发新旧版块 save_post 刷新统计
删除帖子Ajax posts_delete校验 posts_delete 能力和 nonce,执行 wp_trash_post(),触发 bbs_posts_delete
删除版块Ajax plate_delete校验 plate_delete 能力;可选择把帖子迁移到新版块,再把版块移入回收站
恢复帖子或版块Ajax posts_delete_revoke / plate_delete_revoke校验编辑权限,执行 wp_untrash_post(),返回恢复后的链接

plate_delete 在选择迁移帖子时会校验新版块是否存在、是否已删除,并且不能选择当前版块。这个流程能保证迁移后的帖子继续属于有效版块;扩展后台批量清理时也应该保留这几个检查。

批量导入、批量移动、直接修正 plate_id、批量审核评论时,要主动走主题刷新链路。最稳妥的做法是让 save_postwp_update_comment_countposts_views_recordposts_plate_move 正常触发;如果必须后台脚本处理,也要调用对应的 zib_bbs_updata_* 函数并清理相关缓存。

论坛还有动态推荐分缓存,详见 论坛排序与推荐指数。新增统计字段时要同时考虑统计刷新、排序缓存、版块小工具、消息通知和前台列表。

扩展建议

  • 只追加展示内容时,先找 bbs_*_page_*bbs_single_footer
  • 改排序选项时,优先使用 bbs_plate_order_optionsbbs_posts_order_optionsbbs_plate_cat_order_options
  • 改用户页时,使用 author_main_tab_forummain_author_tab_content_forumauthor_favorite_types
  • 改权限时,先读 inc/functions/bbs/inc/user.php,不要绕过 zib_user_can
  • 涉及付费关注、积分评分、采纳奖励、版主操作时,要保留原有业务流程、消息通知和缓存刷新。
  • 论坛页面、发帖页、帖子页和论坛 Ajax 不应被全页缓存固定。

On this page