zibll 隐藏成就体系:一个「发放成功」却从未入账的潜伏 bug

zibll 隐藏成就体系:一个「发放成功」却从未入账的潜伏 bug-知集
zibll 隐藏成就体系:一个「发放成功」却从未入账的潜伏 bug
此内容为付费阅读,请付费后查看
¥1
立即购买
您当前未登录!建议登陆后购买,可保存购买订单
付费阅读
阅读约 30 分钟8,898 字

摘要:站点的「隐藏成就」勋章功能上线几个月,用户敲出秘技后弹窗提示成就已解锁——直到把勋章数据翻开对账才发现:勋章从来没有真正发放出去过。本文完整复盘这个潜伏 bug 的发现、溯源、修复与验证:父主题勋章引擎的注册表机制、「flag 先置位」如何掩盖失败、以及两个极难察觉的存储细节。
适合阅读:zibll 二开站长、给父主题「打补丁」的插件/子主题开发者。

一、发现:弹窗说「恭喜」,账上没有勋章

起因是一次例行核对。子主题的站点彩蛋功能运行了几个月,逻辑很直白:用户敲出 Konami 秘技 → 前端上报 → 服务端授勋 → 弹窗告知。抽检一个用户的勋章列表时,却发现勋章「彩蛋猎人」不在账上——但防重旗标(user_meta)显示这个用户早就「授过」了。

第一反应是缓存。清了对象缓存再看:还是没有。第二反应是存储位置:zibll 的用户 meta 走中央存储优化,换用父主题自己的封装 zib_get_user_meta() 再读——依然没有。这就不是缓存问题了:授予动作本身就没成功过,而且不止一个用户。

二、溯源:父主题勋章引擎的「注册表」机制

翻父主题源码,授勋入口是 zib_add_user_medal($uid, $medal_name, $remarks),核心逻辑两步:

// ① 不能重复添加
if (isset($medal[$medal_name])) { return false; }

// ② 不能添加「不存在」的勋章
$medal_args = zib_get_single_medal_args($medal_name);
if (!$medal_args) { return false; }

关键在②:zib_get_single_medal_args() 从勋章清册里按名字查。清册怎么构建的?

// zib_get_medal_args():读后台配置 → 过 filter → 构建索引
foreach ($cat['items'] as $item) {
    if ($item['name'] && $item['icon'] && $item['get_type']) {   // ← 三者齐全才收
        $item_args[$item['name']] = $item;
    }
}

三个条件缺一不可:name、icon、get_type。任何一个为空,这条勋章会被静默丢弃——不进清册,于是 zib_add_user_medal 在第②步永远返回 false。整个链路没有任何报错、任何日志。

再对照我们的注册代码:为了实现「隐藏成就」(触发条件不公示、只能由代码授予),注册时刻意没传 get_type——直觉上「没有自动触发事件就不该填触发类型」。就是这一个字段,让所有隐藏成就根本没进过清册。

三、v2 为什么错了三层还没被发现

层错误后果
① 注册隐藏成就不传 get_type被清册丢弃 → 授勋必失败
② 顺序先置 flag,再调授勋授勋失败后旗标已落库 → 重试被自己挡住
③ 反馈接口只看 flag 判「已授过」失败被「你已经找到过」掩盖,用户和管理员都以为正常

三层错误互相掩护:第①层让授勋必然失败,第②层让失败不可重试,第③层让失败不可感知。更麻烦的是用户侧体验完全正常——弹窗照样弹、文案照样对,只有对账才能发现。这也是潜伏几个月的原因:这类「成功假象」型 bug 不报错、不崩页面,传统测试全绿。

四、修复:一个哨兵值解决「隐藏」与「可授」的矛盾

难点在于约束冲突:勋章要进清册就必须有 get_type(父主题写死的条件),但隐藏成就恰恰不能有真实的触发类型(否则父主题的自动授予引擎会按类型自动发,违背「仅代码授予」)。解法是哨兵值:

'get_type' => 'zach_hidden'   // 父主题引擎永远不会有此类型的事件
                              // → 能进清册(授勋不再静默失败)
                              // → 引擎永远匹配不到 → 仍然只能由代码授予

同时修正授予顺序与失败回滚:

if (get_user_meta($uid, $flag, true)) { return false; } // 幂等
update_user_meta($uid, $flag, 1);

$ok = zib_add_user_medal($uid, $name, $remark);
if (!$ok) {
    delete_user_meta($uid, $flag);   // 🧨 失败回滚旗标,允许下次重试
    return false;
}

五、顺带踩出的存储细节:raw 读不到的勋章

排查期间还有一个容易误判的点:zibll 对用户 meta 做了中央存储优化,medal_details 这类键不在 user_meta 表里,而是进了统一 option 存储。验证勋章是否到账,必须用父主题封装 zib_get_user_meta();直接 get_user_meta($uid, 'medal_details') 会读到空——你会把「读错了地方」误判成「没发出去」。测试脚本两次在这上面打转。

读法结果结论
get_user_meta($uid, ‘medal_details’)空❌ 读错存储,误判未发放
zib_get_user_meta($uid, ‘medal_details’)勋章数组✅ 唯一正确读法

六、验证:端到端实测代替「看起来对」

修复后用一次性测试用户跑完整链路(服务端断言,非肉眼):

断言结果
首次授勋返回 true通过
二次授勋返回 false(幂等)通过
勋章入账(zib 封装读取,含时间与备注)通过
勋章进清册(get_type=zach_hidden 哨兵)通过
清册里缺失图标/类型的条目被丢弃(回归确认)通过

另有两条 E2E 教训值得记录:① nonce 绑定会话——CLI 里用 wp_set_current_user() 生成的 nonce,配 HTTP Cookie 调用必 403,必须「带 Cookie 抓页面 → 从页面提取 nonce → 同会话回放」;② CLI 环境缺后台函数——wp_delete_user、wp_set_post_thumbnail 都是后台函数,测试脚本要么 require wp-admin/includes/,要么用 update_post_meta 等价替代。

八、踩坑清单

  1. 父主题勋章清册 name && icon && get_type 三全才收,缺一静默丢弃;
  2. 授勋失败无日志无报错,只有对账能发现;
  3. 防重旗标要在授勋成功后才算数,失败必须回滚;
  4. 验证入账用 zib_get_user_meta(),raw 读不到中央存储;
  5. nonce 绑定会话,跨上下文生成的 nonce 配 Cookie 必 403;
  6. CLI 缺后台函数:wp_delete_user / wp_set_post_thumbnail 都要替代方案。

一句话总结:凡是「给父主题引擎投递数据」的扩展,先读引擎的准入条件再动手;授勋类操作要「先授后记、失败回滚」,并把「已授予」的判断建立在账本而不是旗标上。

2026-10-04 更新:本文交付代码覆盖「彩蛋猎人」一枚的注册与授勋。后续把勋章体系补完整时(夜猫子自动成就 + 圣诞礼遇限时三选一活动),又踩了三个”不报错、不留日志、页面看着正常”的静默坑:服务器时区让 date() 判定恒 false、自由文本框 10-4 被严格正则静默拒判、AJAX 授勋后消息角标不会自己变——修法与新交付代码见《勋章活动的三个静默失败:时区、正则、角标》。


📚 zibll 子主题开发实录(共 22 篇) · 系列总目录与阅读路径 →

① 前端性能与加载

② 排版与样式系统

③ 功能实现与边界

④ 系统与运维

⑤ 成就与活动系统

© 版权声明
THE END
喜欢就支持一下吧
点赞9 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容