秘技、限时、自动:三种勋章获取方式,一条授勋通道

秘技、限时、自动:三种勋章获取方式,一条授勋通道-知集
秘技、限时、自动:三种勋章获取方式,一条授勋通道
此内容为付费阅读,请付费后查看
¥1
立即购买
您当前未登录!建议登陆后购买,可保存购买订单
付费阅读
阅读约 39 分钟11,640 字

摘要:给站点做了三枚成就勋章,用了三种完全不同的获取方式——秘技(全站任意页面输入「上上下下左右左右BA」触发)、限时认领(节日窗口内横幅三选一)、自动授予(深夜活跃即得,用户无感知)。三者的交互形态差得很远,但都落到同一条服务端通道上。本文把三种模型各自的「最容易做错的一处」拆开讲:秘技的判定为什么必须在服务端、认领的 pick 为什么是不可信输入、以及自动授予里那个跨零点 + 站点时区的组合拳。附零依赖交付代码。
适合阅读:做过成就/勋章/活动系统,或任何「同一个业务结果、多种触发入口」的开发者。

一、三个模块,三种触发模型

需求来自运营:想让用户有「被记住」的感觉。于是设计了三枚勋章,刻意用了三种不同的获取方式——因为玩法差异本身就是奖励的一部分。

模型触发者用户感知典型场景
① 秘技用户输入一串按键有(探索感)Konami Code「彩蛋猎人」
② 限时认领用户点按钮有(活动感)节日横幅「三选一」
③ 自动授予系统(时段判定)无深夜活跃「夜猫子」

看似三件不相干的事,但它们的后端部分几乎一样:都是「给某个用户,幂等地授予一枚隐藏型勋章」。所以架构上只需要一条通道:

/**
 * 幂等授予:先查旗标 → 再发勋章 → 最后置旗标
 */
function zdw_medal_award_once($uid, $medal_key, $flag) {
    if (get_user_meta($uid, $flag, true)) { return false; }   // 已发过
    if (function_exists('zib_add_user_medal')) {
        if (!zib_add_user_medal($uid, $medal_key)) { return false; }
    }
    update_user_meta($uid, $flag, 1);
    return true;
}

这里有一个顺序问题,值得单独说:幂等旗标必须在发勋章成功之后才置位。如果反过来(先置旗标、再发勋章),一旦发勋章失败,用户就被永久锁在门外——系统认为「已经发过」,而用户手里什么都没有。这个 bug 不会报错,只会让极少数用户在某个偶然时刻永远拿不到勋章。

一条通用原则:幂等标记的意义是「这件事已经完成了」,不是「这件事尝试过了」。两者差一个「成功」的判断,但后果差一个用户。

二、模型①:秘技的判定必须在服务端

「上上下下左右左右BA」这种秘技,前端实现只有十几行:监听键盘、维护一个匹配指针、命中就发个请求。

var seq = ['ArrowUp','ArrowUp','ArrowDown','ArrowDown','ArrowLeft','ArrowRight','ArrowLeft','ArrowRight','b','a'];
var pos = 0;
document.addEventListener('keydown', function (e) {
    pos = (e.key === seq[pos]) ? pos + 1 : (e.key === seq[0] ? 1 : 0);
    if (pos < seq.length) { return; }
    pos = 0;
    report();          // 上报
});

关键:前端只「上报」,不「决定」

天真的做法是前端判中以后直接 ajax → 发勋章。但那样一来,**打开控制台手动调一下端点,就能给自己刷勋章**——成就系统的意义立刻归零。

正确分工是:前端只负责「报告我按对了序列」,服务端独立判断「该不该发」。服务端不看你是不是真的按了键(那没法验证),它只看业务规则:登录了吗?nonce 对吗?之前发过吗?

// 服务端:幂等授予,返回是否"本次新发"
$uid = get_current_user_id();
$new = zdw_medal_award_once($uid, 'zhiji_medal_egg', 'zdw_medal_egg_flag');
wp_send_json(array('ok' => true, 'won' => $new));   // won=false 表示早就有了

页面上的 won 字段只用来决定「要不要放一次特效」,不参与授予决策。这样即使有人伪造上报请求,也只是白白点一下——最多拿到他本来就能拿的那一枚。

教训:任何「用户做了个动作就能拿到奖励」的功能,都要分清「哪些是体验、哪些是规则」。体验可以放前端(越顺滑越好),规则必须留在服务端(越严格越好)。把两者混在一起,就是给自己留了一个可刷的后门。

三、模型②:认领的 pick 是不可信输入

节日活动是「三选一」:雪人、圣诞袜、圣诞树,每人只能领一枚——三枚都送就没有稀缺感了,运营特意强调。

前端渲染三个按钮,点击时带上 pick 参数。服务端如果这样写,就会出问题:

// ✗ 危险:直接把用户传来的值当作勋章 key
$pick = $_REQUEST['pick'];
zib_add_user_medal($uid, $pick);

因为 pick 来自客户端。用户可以传任何字符串:你名单外的勋章、别人的勋章 key、甚至是构造出来让发奖逻辑出错的怪值。正确做法是先过白名单,且**不给”默认兜底”**:

$allow = array('snowman', 'sock', 'tree');
$pick  = sanitize_key($_REQUEST['pick'] ?? '');

if (!in_array($pick, $allow, true)) {
    wp_send_json(array('ok' => false, 'msg' => '请选择一枚勋章'), 400);
}
// 只有命中白名单才继续

注意这里没有写「不在名单里就默认发第一枚」。那种「友好兜底」会让越权请求看起来像是成功了,既掩盖了攻击,也破坏了「用户选了哪枚」这个业务事实。

另外「每人限领一枚」的幂等,就复用第一节那条通道:三枚共用一个旗标——领过任意一枚,其余两枚自动锁定。这正是「三选一」这个业务约束在数据层的表达。

四、模型③:自动授予的两个时间陷阱

「夜猫子」勋章不需要用户做任何事:只要在设定的深夜时段内访问站点,登录会员就自动获得。它挂在 template_redirect 上——这个钩子**只在真正的前台页面请求**时触发,不会在 admin-ajax、REST、cron 里跑(否则后台点一下也会被授予,语义就乱了)。

自动授予看起来简单,但时间判定有两个必踩的坑。

陷阱一:跨零点

「深夜」通常是 00:00 – 05:00,也可能被运营配成 22:00 – 05:00。后者跨过了零点,而下面这个”直觉写法”在跨零点时会恒为假:

// ✗ 跨零点失效:start=22:00, end=05:00 时永远进不去
if ($t >= $s && $t <= $e) { /* 在窗口内 */ }

// ✓ 先判断是否跨零点
if ($s <= $e) {
    return $t >= $s && $t <= $e;      // 同一天内
}
return $t >= $s || $t <= $e;          // 跨零点:落在两端任一区间即在窗口内

陷阱二:服务器时区

第二个坑更隐蔽。取「现在几点」时,很多人顺手写 date('H:i')——而 date() 用的是服务器时钟。服务器常年跑 UTC,站点时区是东八区,两者差 8 小时。于是你在后台把深夜窗口设成 00:00–05:00,实际生效的却是 08:00–13:00——用户在上班时间拿到了”夜猫子”。

// ✗ 服务器时区(UTC):与站点设置差 8 小时
$now = date('H:i');

// ✓ 站点时区:current_time() 已应用站点时区设置
$now = current_time('H:i');

教训:凡是要和「用户看到的钟」对齐的时间判断,都必须用站点时区的取值函数。这类 bug 的特点是:白天测不出来,深夜才复现,而且换个时区的服务器又会变——所以它总在”上线一阵子之后”才被人发现。

五、共同底座:隐藏型勋章只认代码授予

三枚勋章有一个共同点:它们的类型都是隐藏型。父主题有一套「自动成就引擎」(评论数、登录天数、消费金额之类的条件达成即授予),而隐藏型勋章不会被这套引擎产生——只能由代码显式授予。

要点说明
先注册,再授予勋章要在父主题的清册里注册过(含类型、名称、图标),否则授予会被静默丢弃
类型必须是隐藏型这样自动引擎不会”顺手”发给所有人,活动才有意义
授予要走父主题函数子主题不要自己写 meta —— 父主题的勋章存储、缓存、清册刷新都在那个函数里

这也是三个玩法完全不同的模块,最终能共用一条通道的原因:它们唯一的差别只在「什么时候触发」,触发之后的流程完全一致。

六、这类功能怎么验证

断言做法能发现
时段判定(纯函数传值)把判定抽成接受时间参数的纯函数,直接传 23:59 / 00:30 / 12:00跨零点、边界时刻(不用真的等到深夜)
幂等连测两次同一动作连做两次:第一次 won=true,第二次必须 false 且勋章数不变重复授予 / 旗标顺序错
白名单传一个不在名单里的 pick → 应 400 拒绝且勋章数不变越权领取
未登录不带登录态请求上报端点 → 应 401鉴权缺失
开关关闭关掉模块后端点与前台输出应一并消失开关没接上

第一条是这篇文章里最省时间的一条经验:把时间判定抽成「接受时间参数」的纯函数,验证成本从「等到凌晨」降到「传一个字符串」。而且它能覆盖边界——23:59、00:00、05:00、05:01 各测一遍,比真等一夜可靠得多。

附:同系列文章

本文属于「zibll 子主题开发实录」系列,同系列其它篇目见文末导航。


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

① 前端性能与加载

② 排版与样式系统

③ 功能实现与边界

④ 系统与运维

⑤ 成就与活动系统

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

请登录后发表评论

    暂无评论内容