必须放在模型(model)里。控制器只负责接收请求、调用模型方法、返回响应;等级计算涉及数据一致性、事务边界和复用性,一旦写在控制器里,多处触发时易重复出错。

用户等级升降逻辑该放在模型还是控制器里
必须放在模型(Model)里。控制器只负责接收请求、调用模型方法、返回响应;等级计算涉及数据一致性、事务边界和复用性,一旦写在控制器里,下次积分变更、签到、发帖等多处触发时就得重复逻辑,极易出错。
实操建议:
- 建一个
UserRank_model,封装update_rank_by_growth($user_id)方法 - 所有影响成长值的操作(如
add_growth_points())末尾统一调用它 - 方法内先查当前成长值和等级表(
rank_levels),再用ORDER BY min_points DESC找到匹配的最高等级 - 务必用事务包裹:更新成长值 + 更新等级字段 + 记录日志,避免中间失败导致状态不一致
成长值累计后怎么高效匹配等级区间
别用循环遍历等级配置数组,也别每次 SELECT * FROM rank_levels 全查。等级表通常不超过 20 条,但查询频次高,应预加载+二分查找。
实操建议:
- 在模型构造函数中一次性读取
rank_levels表,按min_points升序缓存为$this->rank_levels数组 - 用 PHP 的
array_filter()+end()粗略匹配(适合小数据量);更稳的是手写二分查找——因为min_points是严格递增的 - 示例关键片段:
$levels = $this->rank_levels;<br>$low = 0; $high = count($levels) - 1;<br>while ($low $mid = (int)(($low + $high) / 2);<br> if ($growth >= $levels[$mid]['min_points']) {<br> $rank = $levels[$mid];<br> $low = $mid + 1;<br> } else {<br> $high = $mid - 1;<br> }<br>} - 避免在循环里反复查数据库——这是新手最常卡住性能的地方
等级升/降时如何触发通知或奖励
用观察者模式或简单回调机制,别把邮件发送、推送通知、赠送道具等业务逻辑硬编码进等级更新方法里。
实操建议:
- 在
update_rank_by_growth()结尾加钩子:if ($old_rank !== $new_rank) $this->trigger_rank_change($user_id, $old_rank, $new_rank); -
trigger_rank_change()内部用config_item('rank_hooks')读取配置,动态加载对应类(如RankUp_notifier、RankDown_penalty) - 降级也要处理:比如 VIP 用户掉回普通会员时,需自动取消过期特权(如隐藏广告、专属客服),不能只关注“升级”
- 注意并发:同一用户短时间内多次成长值变动,可能触发多次等级变更。加个
WHERE rank != ?到更新语句里,避免无意义重复执行钩子
CodeIgniter 3 和 4 在实现上的关键差异点
CI4 的依赖注入和实体(Entity)让等级逻辑更易测试,但 CI3 用户别急着升级——现有项目改起来成本不小。
实操建议:
- CI3:用
$this->load->model('UserRank_model')手动加载,成长值字段建议统一用growth_points(别混用exp、score等命名) - CI4:把等级判定写成独立 Service 类(如
App\Services\RankService),通过service('rank')调用;实体中可加getRankNameAttribute()访问器 - 都必须重写
save()方法:在 CI3 的模型update()或 CI4 的update()前插入等级校验,否则直接update()绕过逻辑 - CI4 的
Cache接口更规范,等级配置建议用cache()->get('rank_levels')替代全局变量
真正难的不是算出该升几级,而是保证每次成长值微小变动(比如+1)都不漏触发、不重复触发、不因并发写坏状态。等级表结构是否支持跳跃(如跳过 Lv5 直接到 Lv7)、成长值是否允许负数、降级是否保留历史最高级——这些细节比算法本身更容易在线上出问题。











