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

用户成长与权限体系

扩展子比主题签到、等级经验、认证、徽章、邀请码、用户能力和禁封申诉流程。

模块边界

子比主题的用户成长体系不是一个单独页面,而是一组围绕用户状态运转的能力:

能力主要文件关键数据
签到inc/functions/user/user-checkin.phpinc/functions/user/ajax.phpcheckin_detailcheckin_all_daycheckin_continuous_daycheckin_reward_days
等级经验inc/functions/user/user-level.phplevel_integrallevel_integral_detaillevel_integral_date_detaillevel
用户能力inc/functions/user/user-cap.php_pz('user_cap')、VIP、等级、认证、版主等角色条件
认证inc/functions/user/user-auth.phpinc/functions/user/ajax.phpauthauth_infoauth_apply 消息
徽章inc/functions/user/user_medal.phpinc/functions/user/ajax.phpmedal_detailswear_medaluser_medal_args
邀请码inc/functions/user/invit-code.phpZibCardPassinvit_code、奖励配置
禁封与申诉inc/functions/user/user-ban.phpinc/functions/user/ajax.phpbannedbanned_timebanned_log、申诉消息

二次开发时不要把这些能力写成独立的孤岛。它们已经和用户中心、作者页、评论、论坛、商城、消息通知、邮件通知、微信模板消息、Ajax 弹窗和权限判断连在一起。

签到流程

签到按钮由 zib_get_user_checkin_btn() 输出,前端通过 form-action="user_checkin" 提交到 Ajax 动作:

add_action('wp_ajax_user_checkin', 'zib_ajax_user_checkin');

服务端核心入口是:

zib_user_checkin($user_id);

这个函数会做几件事:

  1. 检查 _pz('checkin_s') 是否开启。
  2. 检查用户今天是否已经签到。
  3. 检查用户是否处于禁封限制状态。
  4. 计算本次积分和经验奖励。
  5. 写入签到明细、累计签到天数、连续签到天数、连续奖励天数。
  6. 触发 user_checkined

签到详情最多保存多少条由 Filter 控制:

$max = apply_filters('user_checkin_detail_maximum', 30);

修改签到明细保留数量:

function zib_docs_checkin_detail_maximum($max)
{
    return 60;
}
add_filter('user_checkin_detail_maximum', 'zib_docs_checkin_detail_maximum');

签到完成后的业务扩展应监听 user_checkined,不要改 Ajax 回调:

function zib_docs_after_user_checkined($user_id, $the_data)
{
    if (!$user_id || empty($the_data['time'])) {
        return;
    }

    zib_update_user_meta($user_id, 'docs_last_checkin_notice', current_time('mysql'));
}
add_action('user_checkined', 'zib_docs_after_user_checkined', 20, 2);

$the_data 包含本次签到奖励:

字段说明
integral本次经验值奖励
points本次积分奖励
time签到时间

后台签到配置主要集中在用户互动的签到 section:

字段作用
checkin_s启用签到功能
checkin_header_user_show是否在菜单栏用户卡片显示签到按钮
checkin_header_user_option.text菜单栏签到按钮文字
checkin_header_user_option.class菜单栏签到按钮颜色
checkin_reward_points单次签到积分奖励
checkin_reward_integral单次签到经验值奖励
continuous_checkin_reward连续签到第 2-7 天的积分和经验奖励

奖励计算由 zib_get_user_checkin_should_reward() 完成。它先读取基础奖励,再用 zib_get_user_checkin_reward_continuous_day($user_id) + 1 判断今天是连续奖励周期中的第几天。如果连续奖励天数超过 zib_get_user_checkin_reward_continuous_max_day(),会回到第 1 天。源码固定最大连续奖励周期为 7 天:

function zib_get_user_checkin_reward_continuous_max_day()
{
    return 7;
}

签到数据分三类保存:

数据保存方式说明
checkin_detailzib_update_user_meta()最近签到明细,按 Y-m-d 作为数组 key
checkin_all_dayupdate_user_meta()累计签到天数
checkin_continuous_dayupdate_user_meta()当前连续签到天数
checkin_reward_dayszib_update_user_meta()连续奖励周期内已签到日期

前台按钮统一用 zib_get_user_checkin_btn() 输出。未登录用户会追加 signin-loader,已签到用户会切换为签到详情入口,未签到用户会追加 initiate-checkin 并提交 form-action="user_checkin"。不要自己拼一个只改 meta 的按钮,否则会绕过禁封、重复签到、防抖、积分和经验联动。

Ajax 写入入口是:

add_action('wp_ajax_user_checkin', 'zib_ajax_user_checkin');

它会依次检查登录态、checkin_s、小黑屋禁封、zib_ajax_debounce('user_checkin', $user_id)、今日是否已签到,然后才调用 zib_user_checkin()。详情弹窗走:

add_action('wp_ajax_checkin_details_modal', 'zib_ajax_checkin_details_modal');

签到小工具和用户排行也会读取 checkin_all_daycheckin_continuous_dayzib_get_user_last_checkin_time()。如果你要做排行榜、任务系统或活动统计,优先读这些封装函数,不要直接解析 checkin_detail 的数组结构。

连续签到奖励周期

checkin_continuous_daycheckin_reward_days 不是同一个概念:

字段用途重置规则
checkin_continuous_day展示当前连续签到天数昨天未签到时重置为 1
checkin_reward_days判断连续奖励第几天超过 7 天或断签时重置

连续天数展示由 zib_update_checkin_continuous_day() 更新。它只关心昨天是否签到:

if (isset($detail[$yesterday_date])) {
    $new_day = zib_get_user_checkin_continuous_day($user_id) + 1;
} else {
    $new_day = 1;
}

update_user_meta($user_id, 'checkin_continuous_day', $new_day);

连续奖励周期由 zib_update_checkin_reward_day() 更新。源码固定最多 7 天,达到最大天数后会重新从第 1 天开始计算奖励:

if (!$detail || !is_array($detail) || count($detail) >= $max) {
    $detail = array();
} else {
    $yesterday = date('Y-m-d', strtotime('-1 day', strtotime($current_date)));
    if (array_search($yesterday, $detail) === false) {
        $detail = array();
    }
}
$detail[] = $current_date;

因此做活动统计时不要把 checkin_reward_days 当成真实连续签到总天数。真实连续天数读 zib_get_user_checkin_continuous_day();本轮奖励第几天读 zib_get_user_checkin_reward_continuous_day();累计签到读 zib_get_user_checkin_all_day()

签到详情弹窗

签到详情弹窗 Ajax 是登录用户专用入口:

add_action('wp_ajax_checkin_details_modal', 'zib_ajax_checkin_details_modal');

zib_ajax_checkin_details_modal() 只做登录态和开关检查,真正 HTML 由 zib_get_user_checkin_details_modal($user_id) 生成。弹窗会组合三块数据:

区块数据来源
用户卡片zibpay_get_user_points()zib_get_user_level()zib_get_user_integral()zib_get_user_checkin_all_day()
今日签到zib_get_user_the_checkin_details()zib_get_user_checkin_btn()
连续奖励continuous_checkin_rewardzib_get_user_checkin_reward_continuous_day()zib_get_checkin_continuous_mini_card()

如果积分系统或等级系统关闭,对应卡片和奖励 badge 会自动不显示。扩展签到弹窗时要保留这个条件判断,不要在积分关闭时强行调用积分展示,也不要在等级关闭时展示经验奖励。

用户中心头部也会在签到开启时显示签到按钮:

if (_pz('checkin_s')) {
    $btns = zib_get_user_checkin_btn('but c-blue ml10 pw-1em radius', '<i class="fa fa-calendar-check-o"></i>' . __('签到', 'zib_language'), '<i class="fa fa-calendar-check-o"></i>' . __('已签到', 'zib_language'));
}

所以新增签到入口时优先复用 zib_get_user_checkin_btn()zib_get_user_checkin_details_link()。这样前端会沿用 signin-loaderinitiate-checkinRefreshModal、移动端底部弹窗和签到完成后返回的 details_link

等级经验

等级经验统一通过:

zib_add_user_level_integral($user_id, $value, $key, $no_limit_day_max);

这个函数会:

  • 检查用户和经验值是否有效。
  • 检查当天经验上限,除非 $no_limit_day_maxtrue
  • 检查禁封限制。
  • 写入经验明细 level_integral_detail
  • 写入每日经验明细 level_integral_date_detail
  • 更新 level_integral

等级升级不是由调用方直接写 level,而是监听用户 meta 更新:

add_action('updated_user_meta', 'zib_update_user_level', 99, 4);
add_action('added_user_meta', 'zib_update_user_level', 99, 4);

level_integral 变化时,主题会根据 _pz('user_level_opt')_pz('user_level_max') 重新计算等级。

给用户增加自定义经验值:

function zib_docs_add_activity_integral($user_id, $activity_id)
{
    if (!$user_id || !$activity_id || !_pz('user_level_s', true)) {
        return;
    }

    $value = 10;
    zib_add_user_level_integral($user_id, $value, 'docs_activity');
}

$key 会写入经验明细,应使用稳定的英文业务标识。不要把可变文案、用户输入或中文说明直接当成 $key

主题内置经验来源

zib_user_level_integral_add 会在等级功能开启后注册一组 Hook:

Hook场景
user_checkined用户签到
user_register新用户注册
admin_init登录后记录登录奖励
save_post发布文章
like-posts文章被点赞
favorite-posts文章被收藏
comment_post发表评论
comment_unapproved_to_approved评论审核通过
like-comment评论被点赞
follow-user用户被关注
bbs_score_extra论坛帖子被加分
bbs_posts_essence_set论坛帖子被设为精华
posts_is_hot帖子成为热门
plate_is_hot版块成为热门
comment_is_hot评论成为热门
answer_adopted回答被采纳

扩展新的经验来源时,优先在你的业务完成后调用 zib_add_user_level_integral()。如果你的业务本身也会触发这些主题 Hook,要避免重复加经验。

论坛经验联动

论坛经验不是在前端按钮里直接增加,而是由 zib_user_level_integral_add 监听论坛业务 Hook。论坛模块还会通过 integral_add_options 给后台经验参数追加一组论坛项目:

add_filter('integral_add_options', 'zib_bbs_integral_add_options_filter');

核心经验项和幂等字段:

事件Hook / 触发点经验 key防重复或上限
发布论坛帖子save_postpost_type=forum_post 且已发布bbs_posts_newpost meta _user_integral_new
创建版块save_postpost_type=plate 且已发布bbs_plate_newpost meta _user_integral_new
帖子被加分bbs_score_extrabbs_score_extrapost meta _user_integral_score_extra,每篇最多 5 次
帖子设为精华bbs_posts_essence_set$val 为真bbs_essencepost meta _user_integral_essence
帖子成为热门posts_is_hotbbs_posts_hotpost meta _user_integral_hot
版块成为热门plate_is_hotbbs_plate_hotpost meta _user_integral_hot
评论成为热评comment_is_hotbbs_comment_hotcomment meta _user_integral_hot
回答被采纳answer_adoptedbbs_adoptcomment meta _user_integral_adopt

这些回调都会从 _pz('user_integral_opt', 0, $key) 读取配置值。配置值为 0 时不会加经验;因此二次开发不要只看 Hook 是否触发,还要检查后台经验项是否启用。

论坛经验和论坛消息通知使用同一批业务 Hook,但各自有独立的防重复字段。例如 answer_adopted 会同时被消息系统、等级经验系统和可能的扩展监听;如果你要新增奖励,应该再写自己的 meta 防重复,不要复用 _user_integral_adoptis_adopted_notify

示例:回答被采纳后给用户额外记录一次成长事件:

add_action('answer_adopted', 'zib_docs_bbs_adopt_growth_record', 30, 2);

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

    if (zib_get_comment_meta($comment->comment_ID, '_docs_adopt_growth_recorded', true)) {
        return;
    }

    zib_update_comment_meta($comment->comment_ID, '_docs_adopt_growth_recorded', true);
    zib_update_user_meta($comment->user_id, '_docs_last_adopt_time', current_time('mysql'));
}

涉及加分、热门、精华、采纳这类操作时,扩展层不要绕过论坛原有 Ajax 和 Hook。直接改 scoreessenceis_hotadopted 只会改变统计结果,可能不会触发经验、消息、徽章和缓存联动。

用户能力判断

主题能力判断统一走:

zib_user_can($user_id, $capability, ...$args);
zib_current_user_can($capability, ...$args);

能力角色来自 _pz('user_cap'),基础判断在 zib_is_can_roles()

条件说明
all所有人可用
logged当前登录用户可用
vip用户 VIP 等级达到要求
level用户等级达到要求
auth已认证用户
is_can_roles自定义角色判断 Filter
zib_user_can最终能力判断 Filter

给自定义角色接入能力系统:

function zib_docs_is_can_roles($is_can, $user_id, $cap_roles)
{
    if ($is_can || !$user_id || empty($cap_roles['docs_editor'])) {
        return $is_can;
    }

    return (bool) zib_get_user_meta($user_id, 'docs_editor', true);
}
add_filter('is_can_roles', 'zib_docs_is_can_roles', 10, 3);

给某个能力做最终兜底:

function zib_docs_user_can($is_can, $user_id, $capability, $args)
{
    if ($is_can || 'docs_manage_resource' !== $capability) {
        return $is_can;
    }

    if (!$user_id || zib_user_is_ban($user_id)) {
        return false;
    }

    return (bool) zib_get_user_meta($user_id, 'docs_manage_resource', true);
}
add_filter('zib_user_can', 'zib_docs_user_can', 10, 4);

写入动作前使用 zib_current_user_can(),展示按钮前也用同一个能力判断,避免按钮可见但提交失败。

认证申请

认证展示和申请入口在 user-auth.php。常用函数:

函数用途
zib_is_user_auth($id)判断是否认证
zib_get_user_auth_badge($id, $class)输出认证图标
zib_get_user_auth_name($id)获取认证名称
zib_get_user_auth_info_link($user_id, $class)输出认证信息弹窗链接
zib_get_user_auth_apply_link($class, $text)输出认证申请弹窗链接
zib_add_user_auth($user_id, $args)写入认证状态和认证资料
zib_user_auth_apply_process($args)处理认证申请

认证信息会通过 author_header_identity 显示在作者头部,也会出现在用户中心认证 Tab。

处理认证申请后,主题会创建站内消息、发送微信模板消息,并触发:

do_action('user_auth_apply_process_newmsg', $msg_arge);

监听认证处理结果:

function zib_docs_auth_apply_processed($msg_args)
{
    if (empty($msg_args['receive_user'])) {
        return;
    }

    zib_update_user_meta((int) $msg_args['receive_user'], 'docs_auth_notice_time', current_time('mysql'));
}
add_action('user_auth_apply_process_newmsg', 'zib_docs_auth_apply_processed');

如果只是展示认证状态,用 zib_is_user_auth()zib_get_user_auth_badge();如果要改变认证状态,应走 zib_add_user_auth() 或后台审核流程,不要只更新一个 auth 字段。

后台认证审核页

认证后台页是 inc/functions/user/admin/auth-page.php,入口是:

users.php?page=user_auth

它查询的不是用户表,而是消息表中的认证申请:

$WHERE = array('type' => 'auth_apply');

if (isset($_REQUEST['status'])) {
    $WHERE['status'] = $_REQUEST['status'];
}
if (isset($_REQUEST['send_user'])) {
    $WHERE['send_user'] = $_REQUEST['send_user'];
}
if (isset($_REQUEST['id'])) {
    $WHERE['id'] = $_REQUEST['id'];
}

$all_count = ZibMsg::get_count($WHERE);
$list      = ZibMsg::get($WHERE, $order, $offset, $ice_perpage, $desc);

列表支持状态筛选、提交用户筛选、搜索、排序和分页。单条记录的 meta 中保存认证名称、简介、申请说明、申请时间和举证图片。点击“立即处理”后,页面会在当前记录下方展开处理表单,提交到同一页面:

if ('process_submit' == $action) {
    $process_submit = array(
        'id'      => $_REQUEST['process_id'],
        'user_id' => $_REQUEST['user_id'],
        'status'  => $_REQUEST['process'],
        'msg'     => $_REQUEST['msg'],
        'name'    => $_REQUEST['name'],
        'desc'    => $_REQUEST['desc'],
    );

    $result = zib_user_auth_apply_process($process_submit);
}

因此扩展认证后台时,核心不是直接写 authauth_info,而是把后台处理动作交给 zib_user_auth_apply_process()。它会负责更新申请消息状态、写用户认证资料、创建处理结果消息,并触发 user_auth_apply_process_newmsg

如果要给认证审核增加一个内部备注字段,可以记录到申请消息的 meta,或在处理结果通知之后写独立日志;不要把后台审核备注直接展示到公开认证信息里:

function zib_docs_auth_apply_processed_log($msg_args)
{
    if (empty($msg_args['receive_user'])) {
        return;
    }

    zib_update_user_meta((int) $msg_args['receive_user'], 'docs_auth_processed_time', current_time('mysql'));
}
add_action('user_auth_apply_process_newmsg', 'zib_docs_auth_apply_processed_log');

后台页里的搜索和筛选来自 $_REQUEST。新增筛选项时要限制字段白名单,并对搜索词做清洗;不要把前端传入的字段名、排序字段或 SQL 片段直接拼进查询。

徽章体系

用户徽章由后台配置、用户 meta、前台展示和 Ajax 操作共同组成。后台入口在“用户设置 -> 用户徽章”,核心文件是 inc/functions/user/user_medal.php,Ajax 写入入口在 inc/functions/user/ajax.php

后台字段作用源码边界
user_medal_s是否启用徽章功能关闭后 zib_get_medal_args() 直接返回 false
user_medal_args徽章分类和徽章明细保存后通过 csf_zibll_options_saved 刷新缓存
reset_user_medal重置全部用户徽章数据后台 Ajax 动作,更新 zib_other_data 中的徽章字段

user_medal_args 是一个分类数组。每个分类包含 cat_nameitems,每个徽章至少需要这些字段:

字段说明
name徽章名称,也是授予、佩戴、索引和去重时使用的标识
desc徽章说明,展示在徽章详情弹窗中
icon徽章图标,默认图标位于 img/medal/
get_type获取方式,自动徽章必须对应 zib_user_float_data 内置静态方法,手动徽章可用 manually_add
get_val达成阈值,自动授予时会用用户数据和它比较

主题默认徽章分为成长、荣誉、成就、限定等类别。后台说明里也明确提示:徽章参数设定后不要随意修改,尤其不要改已经发放过的 name,否则用户已获得的徽章会和新配置对不上。

徽章配置来自 _pz('user_medal_args'),主题会先交给过滤器:

$args = apply_filters('user_medal_args', $args);

再整理为三种索引并写入对象缓存:

索引含义
cat按分类整理徽章,用于按分类展示徽章列表
item按徽章 name 索引,用于校验单个徽章是否存在
getget_type 分组,用于自动授予批量检查

保存主题配置后会刷新缓存:

add_action('csf_zibll_options_saved', 'zib_medal_args_cache_set');

用户侧数据主要有两个字段:

用户数据说明
medal_details用户已获得徽章列表,每项保存 nametimeremarks
wear_medal当前佩戴的徽章名称,未设置时默认取最新获得的徽章

zib_get_user_wear_medal_args() 会先读取 wear_medal,只有该名称仍存在于用户 medal_details 时才返回固定佩戴徽章。如果用户从未设置过 wear_medal,但已经获得过徽章,就默认返回 medal_details 中的第一枚,也就是最新写入的徽章。若后台改名或收回徽章导致 wear_medal 指向不存在的名称,函数会返回 false,不会自动回退到其它旧徽章。

所以扩展徽章迁移或批量收回时,除了处理 medal_details,也要检查 wear_medal 是否还有效。不要只删除徽章明细后留下一个失效的佩戴名称,否则作者页、用户中心和用户名旁徽章都会消失。

zib_add_user_medal() 会先检查用户和徽章是否有效,再读取用户的 medal_details 防重复。授予成功后会触发:

do_action('user_add_medal', $user_id, array_merge($new, $medal_args));

主题已经监听这个 Hook 给用户发送站内消息,消息类型是 medal。如果你要接入额外日志或通知,也应该监听 user_add_medal,不要改写授予函数。

扩展徽章配置时,建议优先使用主题已有 get_type

get_type用户数据来源
registration_time注册天数
new_post发布文章数
like文章获赞数
comment评论数
comment_like评论获赞数
followed粉丝数
favorite文章被收藏数
views文章浏览量
bbs_post论坛发帖数
bbs_post_hot热门帖子数
bbs_essence精华帖子数
bbs_score论坛积分
bbs_plate创建版块数
bbs_plate_hot热门版块数
bbs_adopt回答被采纳数
bbs_comment_hot热门评论数
pay_income付费收入
pay_rebate推广佣金
checkin_all_day累计签到天数
manually_add不参与自动授予,只允许手动授予或业务代码授予

论坛相关徽章的统计来源在 zib_user_float_data 里:

get_type统计函数依赖数据
bbs_postzib_get_user_post_count($user_id, 'publish', 'forum_post')用户已发布论坛帖子数
bbs_post_hotzib_get_user_meta_query_post_count($user_id, 'is_hot', 'forum_post')用户帖子 is_hot meta
bbs_essencezib_get_user_meta_query_post_count($user_id, 'essence', 'forum_post')用户帖子 essence meta
bbs_scoreget_user_posts_meta_sum($user_id, 'score')用户内容累计 score
bbs_platezib_get_user_post_count($user_id, 'publish', 'plate')用户已发布版块数
bbs_plate_hotzib_get_user_meta_query_post_count($user_id, 'is_hot', 'plate')用户版块 is_hot meta
bbs_adoptzib_get_user_meta_query_comment_count($user_id, 'adopted')用户评论 adopted meta
bbs_comment_hotzib_get_user_meta_query_comment_count($user_id, 'is_hot')用户评论 is_hot meta

这些徽章不是监听 Hook 立即授予,而是在自动徽章检查时按当前数据重新统计。论坛批量维护如果直接改 meta,也许能让徽章统计变动,但会漏掉经验、消息、缓存和其他 Hook;因此仍然应走论坛业务函数和 Ajax 流程。

给现有条件增加一个新徽章:

function zib_docs_user_medal_args($args)
{
    if (!is_array($args)) {
        $args = array();
    }

    $args[] = array(
        'cat_name' => __('内容贡献', 'zib_language'),
        'items'    => array(
            array(
                'name'     => __('内容作者', 'zib_language'),
                'desc'     => __('发布文章达到 10 篇', 'zib_language'),
                'icon'     => ZIB_TEMPLATE_DIRECTORY_URI . '/img/medal/medal-1.svg',
                'get_type' => 'new_post',
                'get_val'  => 10,
            ),
        ),
    );

    return $args;
}
add_filter('user_medal_args', 'zib_docs_user_medal_args');

如果徽章来自你的自定义业务,不要把一个不存在的 get_type 写进配置后期待自动授予生效。主题自动检查只会调用 zib_user_float_data 里存在的静态方法,扩展层无法在运行时给这个类补静态方法。更稳的做法是把徽章设为 manually_add,在业务完成时调用主题授予函数:

function zib_docs_user_medal_args($args)
{
    if (!is_array($args)) {
        $args = array();
    }

    $args[] = array(
        'cat_name' => __('特别贡献', 'zib_language'),
        'items'    => array(
            array(
                'name'     => __('专题贡献者', 'zib_language'),
                'desc'     => __('完成专题贡献后获得', 'zib_language'),
                'icon'     => ZIB_TEMPLATE_DIRECTORY_URI . '/img/medal/medal-2.svg',
                'get_type' => 'manually_add',
                'get_val'  => 0,
            ),
        ),
    );

    return $args;
}
add_filter('user_medal_args', 'zib_docs_user_medal_args');

function zib_docs_after_special_contribution($user_id, $post_id)
{
    if (!$user_id || !$post_id || !_pz('user_medal_s')) {
        return;
    }

    zib_add_user_medal($user_id, __('专题贡献者', 'zib_language'), __('完成专题贡献', 'zib_language'));
}
add_action('docs_special_contribution_done', 'zib_docs_after_special_contribution', 20, 2);

授予、收回和佩戴分别使用这些入口:

入口说明
zib_add_user_medal($user_id, $medal_name, $remarks)授予徽章,自动防重复并触发消息 Hook
zib_remove_user_medal($user_id, $medal_name)收回徽章
user_medal_wear登录用户佩戴自己已拥有的徽章
user_medal_manually_add_modal管理端打开手动授予弹窗
user_medal_manually_add管理端手动授予徽章
user_medal_manually_remove管理端手动收回徽章

手动授予要求 _pz('user_medal_s', true) 开启,并且当前用户具备 medal_manually_set 能力。这个能力在后台用户能力配置里定义,默认给管理员角色。不要单独写一个绕过 zib_current_user_can('medal_manually_set') 的授予接口。

user_medal_manually_add_modal 会先验证目标用户存在,再调用 zib_get_user_medal_manually_add_modal($id)。这个弹窗只列出 get_type=manually_add 的徽章;自动徽章不会出现在手动授予列表里。每个徽章同时输出“授予”和“收回”按钮,按钮仍会在提交时二次检查 medal_manually_set

公开展示弹窗和写入动作也要分清:

Ajax action访问范围边界
user_medal_info_modal登录和游客只展示某个用户的徽章列表
single_medal_info_modal登录和游客只展示单个徽章说明;本人拥有时才显示佩戴按钮
user_medal_wear登录用户只能佩戴自己已经拥有的徽章
user_medal_manually_add_modal管理能力只给有 medal_manually_set 能力的人打开
user_medal_manually_adduser_medal_manually_remove管理能力授予或收回目标用户的手动徽章

如果要在业务完成时给用户授予徽章,优先调用 zib_add_user_medal(),让它处理徽章存在性、防重复、消息通知和 user_add_medal Hook。只有后台管理动作才应该走 user_medal_manually_add 这类 Ajax。

主题会在当前用户 Ajax 中执行自动徽章检查:

add_action('ajax_get_current_user', array('zib_auto_add_user_medal', 'init'));

执行过程是:读取 zib_get_medal_args('get'),移除 manually_add,然后逐个检查 method_exists('zib_user_float_data', $get_type)。用户数值大于等于 get_val 时调用 zib_add_user_medal()。因此自动条件必须保持轻量,因为用户中心、浮窗或前台刷新当前用户状态时都可能触发这段逻辑。

徽章展示主要挂在两个位置:

位置Hook
用户主页头部说明user_page_header_desc
作者页头部身份区author_header_identity

前台弹窗和链接使用主题函数输出,例如 zib_get_user_medal_info_link()zib_get_single_medal_info_link()zib_get_medal_wear_link()zib_get_user_medal_info_modal()。如果你要在新位置展示徽章,优先复用这些函数,这样 Ajax 弹窗、佩戴按钮和图标结构能保持一致。

重置用户徽章数据只适合配置误改后的修复场景。后台 reset_user_medal 会批量处理用户 meta,且界面已提示“重置后不可恢复”。上线站点执行前应先备份数据库,并确认徽章配置已经稳定。

邀请码

邀请码使用 ZibCardPass 保存,类型是 invit_code。后台注册设置由 invit_code_s 控制:

模式说明表单表现
close关闭邀请码注册注册表单不显示邀请码
open选填邀请码注册表单直接显示 invit_code 输入框
must必须有邀请码才能注册先显示邀请码验证页,通过后再进入注册表单

说明文案分别来自 invit_code_open_descinvit_code_must_desc。强制邀请码模式会先输出 zib_get_user_invit_code_must_verify_from(),表单里包含人机验证、wp_nonce_field($action, '_wpnonce')action=invit_code_must_verify

强制模式的验证流程:

add_action('wp_ajax_invit_code_must_verify', 'zib_ajax_invit_code_must_verify');
add_action('wp_ajax_nopriv_invit_code_must_verify', 'zib_ajax_invit_code_must_verify');

zib_ajax_invit_code_must_verify() 会先执行:

zib_ajax_man_machine_verification();
zib_ajax_verify_nonce();

再调用 zib_ajax_invit_code_verify() 查询有效邀请码。真正注册时,action/sign_register.php 仍会再次执行 zib_ajax_invit_code_verify(),所以前端通过“继续”按钮不等于已经消耗邀请码。

有效邀请码查询条件是:

ZibCardPass::get_row(array(
    'password' => $code,
    'type'     => 'invit_code',
    'status'   => '0',
));

也就是说,邀请码本质是卡密表中的一种类型,status = 0 表示可用,使用后会更新为 used。注册成功后才会调用:

zib_use_invit_code($user_id, $invit_code_obj);
do_action('zib_use_invit_code', $user_id, $invit_code_obj);

zib_use_invit_code() 会把使用者写入邀请码 meta:

$data = array(
    'id'     => $invit_code_obj->id,
    'status' => 'used',
    'meta'   => array_merge(
        $invit_code_obj->meta,
        array(
            'user_id' => $user_id,
        )
    ),
);
ZibCardPass::update($data);

主题内置的 zib_use_invit_code_reward() 会处理奖励:

奖励 key处理方式
level_integral调用 zib_add_user_level_integral()
points调用 zibpay_update_user_points()
balance调用 zibpay_update_user_balance()
vip调用 zibpay_update_user_vip()

奖励字段来自 CFS_Module::invit_code_reward()。展示奖励说明时使用:

zib_get_invit_code_reward_text($reward, '、');

这里的 reward 是给“使用邀请码完成注册的新用户”的奖励。源码里后台列表曾预留读取 meta.referrer_reward 的变量,但列表列输出被注释,zib_use_invit_code_reward() 也不会处理 referrer_reward。如果要做邀请人奖励,不要只往邀请码 meta 里写 referrer_reward 后期待主题自动发放;应监听 zib_use_invit_code,自己找到邀请人并做幂等发放。

示例:按邀请码创建者给邀请人奖励一次:

function zib_docs_invit_referrer_reward($user_id, $invit_code_obj)
{
    if (!$user_id || empty($invit_code_obj->id)) {
        return;
    }

    $meta = is_array($invit_code_obj->meta) ? $invit_code_obj->meta : array();
    if (!empty($meta['docs_referrer_rewarded'])) {
        return;
    }

    $referrer_id = !empty($meta['create_user_id']) ? (int) $meta['create_user_id'] : 0;
    if (!$referrer_id || $referrer_id === (int) $user_id) {
        return;
    }

    zibpay_update_user_points($referrer_id, array(
        'order_num' => '',
        'value'     => 20,
        'type'      => __('邀请奖励', 'zib_language'),
        'desc'      => __('邀请用户注册奖励', 'zib_language'),
    ));

    ZibCardPass::update(array(
        'id'   => $invit_code_obj->id,
        'meta' => array_merge($meta, array(
            'docs_referrer_rewarded' => current_time('mysql'),
        )),
    ));
}
add_action('zib_use_invit_code', 'zib_docs_invit_referrer_reward', 20, 2);

这个例子只是说明挂载位置和幂等思路。实际邀请人来源要以你的业务为准,可以来自邀请码 meta、用户 referrer_id、推广链接保存的推荐人,或后台创建邀请码时写入的创建者字段。关键是不要重复奖励,也不要在注册前的 invit_code_must_verify 预检阶段发放奖励。

批量创建邀请码用:

zib_generate_invit_code($num, $rand_password, $reward, $remarks);

它会循环调用 ZibCardPass::add(),写入 passwordtype=invit_codestatus=0meta.rewardother 备注。后台邀请码管理页也复用这套能力。

邀请码后台管理页

邀请码后台页是:

users.php?page=invit_code

页面入口在 inc/functions/user/admin/invit-code-page.php,只有超级管理员可访问:

if (!is_super_admin()) {
    wp_die(__('您不能访问此页面', 'zib_language'), __('权限不足', 'zib_language'));
    exit;
}

即使邀请码功能未启用,后台菜单仍会注册,但页面顶部会提示去“用户互动/注册登录”开启 invit_code_s。这点很适合排查:后台能看到邀请码管理,不代表前台注册已经启用邀请码。

后台添加邀请码有两种方式:

方式参数处理
系统自动生成auto_numauto_rewardauto_remarksauto_rand_password_limit调用 zib_generate_invit_code()
导入邀请码import_dataimport_division按行拆分后调用 ZibCardPass::add()

导入时单行格式是:

邀请码 奖励 标识

默认用空格分割,也可以通过 import_division 自定义分隔符。奖励会用 wp_parse_args() 解析到 meta.reward,例如:

P86NpWki level_integral=100 奖励100经验值
P86NpWki balance=10&points=20 奖励余额和积分
auF9D2b4 balance=30&vip=1&vip_time=5 奖励余额和会员

导入写入的数据结构:

ZibCardPass::add(array(
    'password' => $v[0],
    'type'     => 'invit_code',
    'status'   => '0',
    'meta'     => array('reward' => wp_parse_args($_reward)),
    'other'    => !empty($v[2]) ? $v[2] : '',
));

列表页查询同样走 ZibCardPass 表:

$where = array(
    'type' => 'invit_code',
);

if (isset($_GET['status'])) {
    $where['status'] = $_GET['status'];
}

$db = ZibDB::name(ZibCardPass::$table_name);
$db->where($where)->order($orderby, $desc)->page($paged, $ice_perpage);
if ($s) {
    $db->whereLike(array('card', 'password', 'other', 'meta'), $s);
}

列表会展示邀请码、奖励、创建时间、更新时间、状态和标识。status=0 是未使用,status=used 是已使用。已使用记录会从 meta.user_id 显示使用者;如果 meta.shipped_order_idother 中能解析出 shipped_{order_id},还会显示“已售”并跳到商城发货订单。

导出页使用后台 Ajax:

'action' => 'card_pass_export',
'type'   => 'invit_code',

它支持按 status=all|0|used 导出,并支持文本或 Excel 格式。扩展导出时不要另写一套读取逻辑,优先复用卡密导出链路,这样余额卡、积分卡、邀请码和商城自动发货使用的卡密结构能保持一致。

批量删除只删除 type=invit_code

ZibCardPass::delete(array(
    'id'   => $delete_ids,
    'type' => 'invit_code',
));

不要直接按 ID 删除整张卡密表记录,否则可能误删余额充值卡、积分兑换卡或商城自动发货卡密。删除前也要注意:已经售出或已使用的邀请码可能被订单、注册记录或奖励日志引用,生产站点应优先停用或导出备份,而不是直接批量删除。

扩展邀请码使用后的业务:

function zib_docs_use_invit_code($user_id, $invit_code_obj)
{
    if (!$user_id || empty($invit_code_obj->id)) {
        return;
    }

    zib_update_user_meta($user_id, 'docs_registered_with_invite', current_time('mysql'));
}
add_action('zib_use_invit_code', 'zib_docs_use_invit_code', 20, 2);

如果要在商城里销售邀请码,不需要另建一套库存表。商城自动发货支持 auto_delivery_type = invit_code,可以选择从已有邀请码库存中取,也可以按商品配置自动创建邀请码。扩展商品发货时要继续走 ZibCardPass 和商城发货链路,避免用户买到的码和注册验证读取的不是同一批数据。

邀请码验证包含人机验证和 nonce 检查,不要另写一个无校验的验证码查询接口。邀请码只有在用户创建成功后才应被置为已使用,注册前的验证步骤只能检查有效性,不能提前消耗库存。

禁封与申诉

用户禁封状态由:

zib_updata_user_ban($user_id, $type, $info);

写入。注意源码函数名是 updata,扩展时要使用主题现有拼写。

状态字段:

字段说明
banned禁封类型,0 为未禁封
banned_time到期时间,空值通常表示长期有效
banned_log当前记录和历史记录

禁封状态变化后触发:

do_action('updata_user_ban', $user_id, $type, $info);

主题默认会通过这个 Hook 给用户发送站内消息和邮件通知。

监听禁封变化:

function zib_docs_user_ban_changed($user_id, $type, $info)
{
    if (!$user_id) {
        return;
    }

    zib_update_user_meta($user_id, 'docs_ban_changed_time', current_time('mysql'));
}
add_action('updata_user_ban', 'zib_docs_user_ban_changed', 20, 3);

禁封会影响多个位置:

  • zib_user_can 最终能力判断。
  • 头像和用户名标识。
  • 作者头部身份标识。
  • 用户中心头部说明。
  • 登录限制。
  • 签到和经验获取。

因此不要只在某个表单提交前检查禁封;需要统一走 zib_user_can()zib_current_user_can()zib_user_is_ban()

后台申诉与举报处理页

禁封申诉和用户举报共用后台页 inc/functions/user/admin/ban-page.php,入口是:

users.php?page=user_ban

它默认查询两类消息:

$WHERE = array('type' => array('user_report', 'ban_appeal'));

页面支持按类型切换:

参数消息 type含义
type=banban_appeal用户禁封申诉
type=reportuser_report用户举报

也支持 statussend_useridreport_user 和搜索词筛选。report_user 会查询消息 meta 中的 report_user_id_ 标记,用于查看某个用户被举报的记录。

提交处理时通过隐藏字段区分处理类型:

if ('process_submit' === $action && $process_type) {
    $process_user_id = !empty($_REQUEST['user_id']) ? (int) $_REQUEST['user_id'] : 0;
    $process         = !empty($_REQUEST['process']) ? (int) $_REQUEST['process'] : 0;
    $desc            = isset($_REQUEST['desc']) ? trim(strip_tags($_REQUEST['desc'])) : '';
    $process_id      = !empty($_REQUEST['process_id']) ? (int) $_REQUEST['process_id'] : 0;

    if ('user_report' === $process_type) {
        $send_user = !empty($_REQUEST['send_user']) ? $_REQUEST['send_user'] : 0;
        zib_user_report_process($process_id, $send_user, $desc);
    } else {
        zib_user_ban_appeal_process($process_id, $process_user_id, $process, $desc);
    }
}

举报处理只把举报消息标记为处理完成,并把处理说明反馈给举报人;是否禁封被举报用户,需要管理员根据页面中展示的用户状态另行进入用户主页或禁封弹窗处理。申诉处理则可能解封用户:通过时走 zib_user_ban_appeal_process(),拒绝时保留原禁封状态并回写处理留言。

扩展这类后台流程时要保留“两段式”边界:

  1. 举报是举报消息处理,不自动等于禁封。
  2. 禁封申诉是对既有禁封状态的复核,不应重新生成一条无来源的禁封记录。
  3. 页面展示可读取 zib_get_user_ban_info(),写入必须走 zib_user_report_process()zib_user_ban_appeal_process()zib_updata_user_ban()

例如在举报处理完成后补一条审计记录,可以监听处理函数创建的 user_report_reply 消息。不要把举报人的举证图片、违规链接和后台处理说明输出到公开作者页:

function zib_docs_user_report_reply_message($values)
{
    if (empty($values['type']) || 'user_report_reply' !== $values['type']) {
        return;
    }

    update_option('docs_last_user_report_process_time', current_time('mysql'), false);
}
add_action('zib_add_message', 'zib_docs_user_report_reply_message');

Ajax 动作清单

Ajax action登录要求用途
user_checkin登录当前用户签到
checkin_details_modal登录签到详情弹窗
user_auth_info_modal可公开用户认证信息弹窗
user_auth_apply_modal登录认证申请弹窗
user_auth_apply登录提交认证申请
user_medal_info_modal可公开用户徽章明细弹窗
single_medal_info_modal可公开单个徽章说明弹窗
user_medal_wear登录佩戴徽章
user_medal_manually_add_modal管理能力手动授予徽章弹窗
user_medal_manually_add管理能力手动授予徽章
user_medal_manually_remove管理能力手动收回徽章
set_user_ban_modal管理能力禁封设置弹窗
set_user_ban管理能力更新禁封状态
user_ban_info_modal本人或管理能力禁封信息弹窗
user_ban_appeal_modal本人禁封申诉弹窗
user_ban_appeal本人或游客场景提交申诉
invit_code_must_verify可公开注册时验证邀请码

公开弹窗只应展示公开资料。涉及写入的 Ajax 必须校验 nonce、登录态、能力和目标用户。

扩展建议

需求推荐入口
签到后同步外部系统user_checkined
增加自定义经验来源业务完成后调用 zib_add_user_level_integral()
扩展能力角色is_can_roles
最终拦截某个能力zib_user_can
认证处理后发送额外通知user_auth_apply_process_newmsg
增加徽章配置user_medal_args
邀请码使用后记录业务数据zib_use_invit_code
禁封状态变化后同步审计updata_user_ban

风险清单

  • 不要直接更新 level,应更新 level_integral 或调用等级经验函数。
  • 不要把经验 $key 写成用户可控内容。
  • 不要给禁封用户绕过 zib_user_can() 的写入入口。
  • 不要在公开 Ajax 中泄露手机号、邮箱、余额、订单、申诉详情或后台处理备注。
  • 不要在当前用户 Ajax 自动徽章检查里做重型查询。
  • 不要自行生成无状态的邀请码,主题的邀请码依赖 ZibCardPass 状态和奖励 meta。
  • 不要只做前端隐藏按钮,敏感操作必须在服务端再次判断能力。

On this page