本文由 FlintHub 开发文档归档发布。原始文档日期见标题前缀,来源:暂存20260913/docs/points_level_api.md。
Points 积分等级系统 — 插件开发者接口参考
版本:1.0(2026-09-12)
依据:《积分等级系统(Points Lv1-11)对外接口审计》(只读审计,未改核心代码)
适用范围:需要在插件中消费「积分等级」的第三方插件
配套代码:
app/Helpers/Points.php、app/Helpers/Auth.php、app/Models/User.php、app/SplitDB/Schema.php
一、数据模型速览
| 数据 | 位置 | 说明 |
|---|---|---|
users.points / users.level | 核心库 users 表 | 快照:level 仅在 Points::award() 升级时写入;Points::deduct() 不降级 |
level_config 表 | app/SplitDB/Schema.php L477-485(建表)、L176-185(种子) | 阈值配置(非硬编码、非 settings 键),后台 /admin/levels 可改 |
| 默认阈值 | Points.php L273-278 | Lv1-11 = 0/10/40/90/160/250/360/490/640/810/1000 |
两条必须知道的事实:
- 等级只升不降:升级判定在
Points.phpL91($newLevel > current),扣分不触发降级。 users.level是快照,不是权威值:管理员在后台改level_config阈值后,快照不会重算;Points::calculateLevel($points)才是按当前阈值的权威值。
二、推荐接口清单(插件可放心调用)
1. 单用户等级摘要(权威重算)— ✅ 推荐
$summary = \app\Helpers\Points::getSummary($userId); // ?array 或 null(用户不存在)
$level = $summary['level'] ?? 0; // 按当前阈值重算的等级
$points = $summary['points'] ?? 0;
$next = $summary['next_level_points'] ?? 0; // 下一级所需积分
$progress = $summary['progress'] ?? 0; // 距下一级进度 0-100- 位置:
app/Helpers/Points.phpL211-241 - 说明:内部查一次
users+ 全量level_config,适合独立页面 / 控制器;不要放每页必跑的钩子里(违反每请求 ≤5 查询的钩子预算)。
2. 当前登录用户 — ✅ 推荐
$me = \app\Helpers\Auth::getCurrentUser(); // ?array
$level = (int)($me['level'] ?? 0); // 快照
$points = (int)($me['points'] ?? 0);- 位置:
app/Helpers/Auth.phpL29-54 - 说明:零额外查询(请求级缓存),读的是
users.level快照。
3. 批量取用户等级(帖子楼层 / 列表)— ✅ 推荐
$users = \app\Models\User::batchGetUsers($uids); // [uid => ['username','avatar','level']]
if (($users[$uid]['level'] ?? 0) >= 5) { /* 特权 */ }- 位置:
app/Models/User.phpL344-378 - 说明:请求级缓存,插件开发规范强制:批量补用户名/头像/等级一律走它,禁止直查
users表。 - 同一批量数据也随核心列表自动附带:
$thread['level'](app/Models/Thread.phpL916)、$post['level'](app/Models/Post.phpL436)。
4. 读取等级阈值(Lv1-Lv11 各档积分门槛)— ✅ 推荐
$configs = \app\Helpers\Points::getAllConfigs(); // [{level,title,required_points,icon,color}] 升序
$lv5Points = $configs[4]['required_points'] ?? 160; // 索引 4 = Lv5- 位置:
app/Helpers/Points.phpL43-63 - 说明:唯一推荐入口。DB 无数据时回退默认公式(
(level-1)^2 * 10,与种子一致)。 - ⚠️ 不要用
Points::getLevelConfig()(L13-41)拿阈值——它只返回title/color/icon,不含required_points,且默认数组只写到 Lv10(Lv11 靠 L39 特判补齐)。
5. 积分 → 等级重算(权威值)— ✅ 推荐
$level = \app\Helpers\Points::calculateLevel($points); // int,按当前阈值重算- 位置:
app/Helpers/Points.phpL65-74 - 说明:纯函数,不查库。适合"已知积分、要拿当前档位"的场景。
6. 渲染等级徽章 HTML — ✅ 推荐
echo \app\Helpers\Points::renderBadge($level); // 输出 span.level-badge,已做色值 XSS 校验
$color = \app\Helpers\Points::getLevelColor($level); // 单取色值- 位置:
app/Helpers/Points.phpL197-209、L192-195 - 说明:核心右栏发帖人面板(
app/Views/_components/right_sidebar.phpL72)、user_profile 插件个人主页(plugins/user_profile/views/front.phpL48)均用它。
7. 写积分(自动升级 + 通知 + 钩子)— ✅ 推荐
\app\Helpers\Points::award($userId, $points, $reason, $relatedId, $relatedType); // 加积分,自动升级
\app\Helpers\Points::deduct($userId, $points, $reason, $relatedId, $relatedType); // 扣积分(条件原子扣减),返回 bool- 位置:
app/Helpers/Points.phpL76-124、L126-175 - 说明:插件开发规范红线:禁止直写
users.points/level,增减积分一律走Points::award()/deduct()。
三、4 个真实缺口(当前需绕行的点)
缺口 1:等级变化没有插件钩子
Plugin::hook('level_up') 之类的事件不存在。升级只发生在 Points::award() 内(L90-105),外部唯一可用的关联事件是 points_award_after(L110-116,随调用方事务),但参数只有 user_id/points/reason/related_id/related_type,不含 old_level / new_level——插件要感知"谁升了级"必须自己先查一次 users.level 再对比,且无法精确知道升级前的等级。
另注意 Notification::TYPE_LEVEL_UP(app/Helpers/Notification.php L28)注释仍标"预留",但实际已在 L98 触发写入 notifications 表。
缺口 2:批量等级摘要缺失
批量接口只有 User::batchGetUsers() 返回的快照 level,没有 title/icon/next_level_points/progress 等展示字段。要批量渲染"等级徽章 + 进度"只能逐个调 getSummary()(N+1)或自行联查 level_config。
缺口 3:快照 vs 权威值口径不一致
users.level/$thread['level']/batchGetUsers()→ 快照(后台改阈值后失准);Points::calculateLevel()/getSummary()→ 按当前阈值重算(权威)。- 两者在管理员改过
/admin/levels后会产生分歧。做权限判断时请明确口径,推荐以重算值为准。
缺口 4:核心模板无统一等级徽章占位
- 帖子详情
thread/show与首页/论坛/分类/标签列表页不渲染核心等级徽章(home/index_default.phpL38、forum/index.phpL50 算了$level但未输出)。 - 现有一族"插件注入占位":
$wearingMedals(medal)、$pmTitles(points_mall)、$ulBadges(user_level),均为[uid => HTML],由各插件在controller_view_before注入、核心模板在用户名旁输出(thread/show.phpL93-94、L239-240)。 - 新插件要展示等级徽章,需自行走"controller_view_before 注入 → 复用既有占位或自建变量"的模式。
四、Lv5+ 特权参考实现(组合以上接口)
// 场景 A:单用户门槛(权威重算)
$level = \app\Helpers\Points::getSummary($userId)['level'] ?? 0;
// 场景 B:当前登录用户(快照,零查询)
$level = (int)(\app\Helpers\Auth::getCurrentUser()['level'] ?? 0);
// 场景 C:批量(帖子楼层、列表页)
$users = \app\Models\User::batchGetUsers($uids);
foreach ($users as $uid => $u) { if ($u['level'] >= 5) { /* 该楼层放行特权 */ } }
// 场景 D:动态读 Lv5 阈值(阈值可后台改,用配置而非硬编码 160)
$lv5Points = \app\Helpers\Points::getAllConfigs()[4]['required_points'] ?? 160;
if ($points >= $lv5Points) { /* 特权 */ }五、待办:建议补充接口(暂不实施,第三周目重启评估)
按优先级排序,实施前逐个评估。仅建议,未列入任何版本计划。
| 优先级 | 建议接口 | 解决缺口 | 说明 |
|---|---|---|---|
| P0 | 等级变更钩子 Plugin::hook('level_changed_after'),参数 user_id / old_level / new_level / points,在 Points.php L91-105 升级成功后触发 | 缺口 1 | 插件无需自查对比即可感知升级 |
| P1 | Points::levelOf(int $userId): int(请求级缓存) | 缺口 3 | 统一"单用户当前等级"入口,消除快照/重算两套口径与重复 SQL |
| P1 | Points::isAtLeast(int $userId, int $minLevel): bool | 缺口 3 | "LvN+ 特权"类需求的直接落点 |
| P2 | Points::getSummaries(array $ids): array | 缺口 2 | 批量摘要(含 title/icon/progress),现在批量只有快照 level |
| P2 | 核心统一等级徽章占位 $data['levelBadges']([uid => HTML],核心批量填充、插件可挂钩覆盖) | 缺口 4 | 消除各插件自造注入变量的重复模式 |
| P3 | 阈值变更后快照重算联动,或文档明确口径 | 缺口 3 | saveLevelConfig 改阈值后 users.level 快照失准 |
本文档基于只读审计生成,审计时未修改任何核心或插件代码。