• one

    昨天下午+晚上3个40余轮对话共消耗4000+积分精细打造(未完工),刚又适配到9.1。

    我的想法是后期我制作的所有和权限相关插件都将基于此插件制作。(本插件扩展 API(私有钩子)),但个人的能力和想法有限,征集下大家的想法一起来完善。

    • 积分等级体系:等级为积分的纯函数(从不落库),达标自动晋升、跌破自动回落;默认档位 LV0=0 / LV1=30 / LV2=100 / LV3=300 / LV4=1000,后台可任意增删改(名称唯一、门槛递增、首档自动归零、上限 20 档)。
    • 用户状态域(单一状态链):骗子 > 禁访 > 禁言 > 停用 > 管理 > 特殊 > LV,一人同一时刻仅呈现一种状态;骗子/禁访互斥(入组承接对方 prev_flag,保证恢复链不中断)。
    • 等级/状态徽章:帖子详情页与列表页作者旁显示徽章;三色(边框/底色/文字)后台可配,仅接受 #RRGGBB 白名单色值。
    • 版块等级门槛:按版块独立配置 查看/发帖/回帖 三项 LV 门槛;版块页与详情页直接拦截,混合列表(首页/搜索/个人主页)逐行隐藏;管理组与特殊组直通。
    • 停用组只读:user.can_speak 返回自定义提示语,保留浏览权限但禁止发言。
    • 内容遮罩:组级开关,开启后该组成员历史主贴/回帖正文替换为禁止图标(仅改渲染,移出组即恢复)。
    • 个性签名:Markdown 渲染(复用内核 markdown_html,自带 XSS 防护)、仅登录可见、骗子隐藏、两行高截断;个人资料页编辑面板带实时字数统计与防抖 AJAX 预览;红区仅埋占位符,page.before_render 整页一条 IN 查询批量回填;AJAX 新楼层路径就地单作者渲染。
    • 升级/降级站内通知:user.points_changed O(1) 门槛比对,措辞中性,各自独立开关。
    • 前台快捷操作:主题页管理员行内 禁言(含时长档位)/解禁/禁访/解禁访/骗子/移出骗子,统一理由弹窗(每页一次),levels_op 路由白名单分发并全量复审权限。
    • 禁言到期自动解除:app.boot 惰性清理,60 秒原子节流(条件 UPDATE 抢占 + rowCount 认领),永久禁言 expires_at=0 不误判。
    • 成员管理:后台五区(等级与徽章/成员管理/版块限制/操作记录/设置),用户名联想搜索、按组筛选、分页;标志位(is_banned/is_muted)写入与原值恢复全在事务内。
    • 审计日志:每次状态变更一行(含到期自动解禁,operator=0),理由必填;后台按动作筛选、CSV 导出(id 游标分块 + UTF-8 BOM + 公式注入防护)、保留期自动清理(1/50 概率防写放大)、一键清空。
    • 公开管理日志:可开关的前台页,仅公开 禁言/禁访/骗子 类动作(停用/特殊不公开),自然语言事件句。
    • 侧栏等级卡片与 /levels 等级页:当前徽章、积分、升级进度条、下一等级差距、等级一览表。
    • 扩展 API(私有钩子):levels.query(查询用户等级/状态)、levels.mute_set/mute_clear/ban_set/ban_clear(供其他插件编程调用成员管理)。
    • 卸载语义:保留数据时一切原样(标志位与成员关系继续生效于内核);删除数据时先按 prev_flag 恢复全部标志位再清表。

    01.png
    02.png
    03.png

    主楼
  • one

    1. 设计理念

    • 等级是积分的纯函数:等级从不落库、从不迁移,levels_of_points() 实时按门槛计算,后台改档位即刻全站生效。没有"升级任务"、没有状态同步、没有边界 bug——积分变,等级变。
    • 单一状态链:一个用户同一时刻只呈现一种状态,优先级 骗子 > 禁访 > 禁言 > 停用 > 管理 > 特殊 > LV。命中高优先状态即完全遮蔽低优先展示(如骗子不再显示 LV),杜绝"既是骗子又是 LV3"的观感混乱。
    • 复用内核标志位:禁言/禁访不另建权限体系,直接写内核 is_muted / is_banned(写入时记录原值 prev_flag,移出组时精确恢复);内核自身的封禁判断、状态标签、登录拦截全部自动生效,权限模型随核心演进零漂移。
    • 红区零 DB:渲染循环 Hook(before/after_render、content_after、can_speak、topic.actions)内只读请求级缓存与纯函数——三张业务表(成员/组/规则)在写入时镜像为 JSON 进 app_settings,读取走 settings_cache(每请求一次全量设置查询,零额外开销);行内用户字段(group_id/points/is_banned/is_muted)由核心 attach_users 预载。
    • 占位符回填:签名在红区只埋 HTML 注释占位符(含每请求随机 token),page.before_render 收尾时一条 IN 查询批量取全部签名并正则回填,替换 O(n) 行渲染为 O(1) 次查询。
    • 服务端兜底,前端仅交互:JS 只负责弹窗显隐、字数统计、防抖预览、后台表格行克隆;levels_op 路由统一 need_login + require_post + can_manage + 目标保护复审,被绕过不构成安全漏洞。

    2. 权限模型

    能力判定
    等级门槛豁免管理组成员(uid=1 或用户组 allow_manage=1)与特殊组成员直通所有版块门槛
    版块 查看/发帖/回帖版块规则下标 ≤ 用户等级下标;发帖在 topic.before_save、回帖在 reply.before_save(一次 forum_id 查询)、查看在版块页 topic.index_data.loaded(err 拦截)与详情页 topic.before_render。1.0.1 起前置提示:回帖表单在详情页即被替换为提示盒(reply.form_extra + page.before_render,等级不足无从输入);发帖页经 topic.form_extra 注入提示条 + 版块门槛映射,JS 按所选版块联动禁用提交按钮(无 JS 降级为提交时拦截)
    混合列表过滤topic.before_render(list 上下文)标记 __levels_list_hidden + topic.after_render 返回空串清行(核心不回收 index_data.loaded 的返回值,不能在该钩子内改 rows)
    停用组只读user.can_speak 返回提示字符串(内核非 true 即拒绝发言),浏览权限保留;主题页回帖框文案由 page.before_render 按停用状态替换(内核 can_speak() 只返回 bool,硬编码「当前用户禁止发言」,插件的字符串提示仅在提交时经 need_speak 呈现)
    前台快捷操作can_manage() 渲染 + front_ops_enabled 开关;levels_op 路由服务端复审
    操作目标保护levels_row_can_target()(内存版,红区):uid=1 绝对保护;管理组成员仅创始人(或 allow_admin_targets 开关开启)可操作。写路径用查库版 levels_can_target() 复审
    后台五区 / CSV / 用户联想need_admin()
    公开日志所有登录用户;public_logs_enabled 开关控制,仅公开 禁言/禁访/骗子 动作
    签名仅登录可见(uid());骗子/内容遮罩作者隐藏(1.3.8);停用/禁言正常显示

    3. 架构与模块划分

    app/plugins/levels/plugin.php(单文件插件,约 2390 行)
    ├── 配置          levels_config / levels_default_defs /
    │                 levels_defs / levels_color          static 请求级缓存;门槛排序去重、
    │                                                         首档归零、20 档上限;色值 #RRGGBB 白名单
    ├── 运行时快照    levels_runtime / members_of / group_cfg /
    │                 rule_of / cache_rebuild             三张表 JSON 镜像进 app_settings;
    │                                                         写路径变更后必调 rebuild 刷新镜像与快照
    ├── app.boot      levels_boot / expire_mutes           预载快照 + 禁言到期惰性清理
    │                                                         (60 秒条件 UPDATE 原子节流 + rowCount 认领)
    ├── 状态链解析    levels_row_state / row_is_manage /
    │                 me_bypass / row_can_target           纯内存判定;返回最高优先状态 key
    ├── 展示          levels_badge_html / row_badge /
    │                 inject_badge / apply_mask            徽章注入(位置可选:信息行开头/末尾/作者名后,
    │                                                         未命中逐级回退;列表行固定信息行开头,
    │                                                         受列表页开关控制);遮罩替换 post-content
    ├── 门禁          levels_gate_allows / gate_err /
    │                 topic_before_save / reply_before_save /
    │                 index_loaded / topic_before_render /
    │                 topic_form_extra / reply_form_extra   三项门槛统一错误语;
    │                                                         混合列表标记 + after_render 清行;
    │                                                         1.0.1 发帖提示条 + 回帖表单前置替换
    ├── 停用组        levels_can_speak                     user.can_speak 字符串提示
    ├── 签名          signature_html / allowed / placeholder /
    │                 page_render / profile_form /
    │                 signature_save / signature_preview    占位符批量回填 + AJAX 单楼层就地渲染 +
    │                                                         防抖预览(与最终渲染同一函数)
    ├── 通知          levels_points_changed                user.points_changed + points_manager.points_changed 双事件 O(1) 门槛比对
    ├── 成员管理      member_set / member_remove /
    │                 can_target                           事务内标志位写入/原值恢复;
    │                                                         骗子↔禁访互斥承接 prev_flag
    ├── 审计          levels_log / log_sentence /
    │                 log_actions / log_public_actions     每次变更一行;1/50 概率清理过期;
    │                                                         自然语言事件句供公开日志
    ├── 扩展 API      levels_hook_query / mute_set /
    │                 mute_clear / ban_set / ban_clear      私有钩子,供其他插件 fire()/hook() 调用
    ├── 前台快捷操作   quickops_html / op_icon / inject_quickops /
    │                 modal_html / op_route / topic_actions 注入位置统一为「引用回复」之前;
    │                                                         入组三键图标化;弹窗每页一次;
    │                                                         目标保护内存判定;路由白名单分发 + 服务端复审
    ├── 页面          levels_page / public_logs_page /
    │                 logs_download / search_users          /levels 等级页、公开日志页(分页)、
    │                                                         CSV(id 游标分块)、用户联想
    ├── 后台五区      levels_admin_page + 5 个子页渲染/保存   等级与徽章 / 成员管理 / 版块限制 /
    │                                                         操作记录 / 设置;POST 白名单分发
    ├── 安装/卸载     levels_install / levels_uninstall     五张自有表 + 组配置种子(幂等);
    │                                                         卸载先恢复标志位再清表,运行时标记
    │                                                         (节流戳)随镜像一并复位(1.3.4)
    └── 资产          levels_css / levels_js                levels- 前缀;JS 具名 IIFE 仅交互

    4. 数据模型

    表用途关键列
    plugin_levels_groups六个固定组的徽章三色与内容屏蔽开关group_key(PK)、name、border/bg/text、mask
    plugin_levels_members手动组成员(每用户每组一行)UNIQUE(user_id,group_key)、reason、operator_id、expires_at(禁言专用,0=永久)、prev_flag(内核标志位原值)
    plugin_levels_forum_rules版块三项 LV 门槛(下标,0=不限制)forum_id(PK)、view_level/post_level/reply_level
    plugin_levels_signatures个性签名user_id(PK)、content、updated_at
    plugin_levels_logs审计日志action(11 种)、operator_id(0=系统/到期)、target_id、detail(用户名[:到期戳])、reason、created_at

    另有 app_settings 三个 JSON 镜像键(plugin_levels_members_json / _groups_json / _rules_json)与节流键 plugin_levels_last_expire_check。

    5. 关键机制说明

    5.1 等级纯函数

    function levels_of_points(int $points): int
    {
        $li = 0;
        foreach (levels_defs() as $i => $d) {
            if ($points >= $d['points']) $li = $i;
            else break;
        }
        return $li;
    }

    levels_defs() 保证门槛严格递增,因此首档命中即短路。升/降级通知同样基于新旧两次纯函数结果的比对,无任何存储状态。

    5.2 状态链与内核标志位的分工

    • 内核标志位(is_banned/is_muted)是权限事实:内核登录拦截、发言拦截、状态标签全部依赖它;
    • 本插件的成员表是管理事实:谁被谁为何移入哪个组、何时到期、标志位原值是多少;
    • 状态链解析在渲染层合并两者:骗子组成员即使带着 is_banned=1 也只显示骗子徽章(before_render 压掉渲染副本的标志位,不动库)。

    5.2.1 单一状态强制(1.0.2 / 1.2.3)

    四个处罚组(骗子/禁访/禁言/停用)与特殊组在数据层互斥:levels_member_set 入任一组前,事务内先移出该用户的对方集合组成员关系——入处罚组时清除其他处罚组 + 特殊组;入特殊组时清除所有处罚组。处罚组按各自 prev_flag 恢复标志位(停用/特殊无内核标志位直接删记录);新组的 prev_flag 承接恢复后的当前标志位,保证任意顶替顺序下恢复链不中断。每个被顶替组各记一行「移入X组自动解除」审计。这消除了"先禁言再停用 → is_muted 残留"以及"特殊组与骗子组共存"一类叠加态。manage 组是内核身份(group_id 的 allow_manage),不在本插件管理范围,不参与互斥。

    存量自愈(1.2.3)(已于 1.3.7 移除):levels_mutex_self_heal() 曾用于修复 1.2.2 之前共存态的存量脏数据;新装环境互斥由数据层从源头保证,无迁移负担,纯死代码已清理。

    5.3 禁言到期惰性清理

    继承 topic_manage 已验证的模式:app.boot 时从内存快照筛出已到期记录 → 条件 UPDATE 抢占 60 秒节流窗(并发仅一个请求真正执行)→ 逐用户事务内 rowCount 认领翻转 is_muted 并删成员行 → 审计一行 operator=0 → 重建镜像。无到期记录的请求零 DB 触碰。1.3.4 起审计与镜像重建只对事务内认领成功(tx() 返回 bool)的 uid 执行——此前按事务外 SELECT 结果记日志,节流戳首次初始化的并发窗口内可能重复记录 mute_expire。

    5.4 混合列表过滤(关键坑位)

    核心 topic_index_data() 对 topic.index_data.loaded 的返回值不回收(fire-and-forget),在该钩子里改写 rows 无效。正确通道是列表渲染链:topic_list_row() 对每行先过 topic.before_render(list 上下文),返回的 row 继续传入 topic.after_render——因此 before_render 标记 __levels_list_hidden、after_render 见标记返回空串即可整行清除,与 topic_read_groups 的隐藏机制同构。

    5.4.1 门槛前置提示(1.0.1)

    提交时拦截(before_save)只保证安全,不保证体验——等级不足的用户会在辛苦编辑后才收到拒绝。前置提示分两处:

    • 回帖(硬阻止):reply.form_extra(核心仅在 can_speak 且组权限通过时触发,ctx 带 topic 行含 forum_id)以纯内存判定等级不足时置全局标记;levels_page_render 据此把 <form class="ajax-reply-form"> 整体替换为核心「无权限」同形态的 reply-login-box disabled 提示盒。用户无从输入,杜绝白写。访客/停用用户渲染的本来就是提示盒,不经过该钩子。
    • 发帖(软提示 + 联动禁用):topic.form_extra(新建/编辑均触发,ctx 带 topic 行)注入 .levels-gate-hint 提示条——服务端按表单初始 forum_id 预渲染文案并决定初始显隐;因版块下拉可切换(服务端无法预知最终选择),同时携带全版块发帖门槛映射 data-levels-post-gates(仅含 post_level>0 的版块,JSON 经 h() 属性转义)。JS 从提示条 closest('form') 定位版块 select 与提交按钮:change 时按映射显隐提示并 disabled/恢复按钮,初始执行一次同步状态。无 JS 时降级为提交时拦截。
    • 文案统一走 levels_gate_hint()(view/post/reply 三动词),与提交时 levels_gate_err() 完全一致;JS 端重建文案仅用于切换版块后的更新。

    5.5 签名占位符回填

    红区 content_after 只输出 <!--levels-{token}-{uid}-->(token 每请求随机,防内容伪造命中);page.before_render 收集全部待回填 uid,一条 IN 查询取签名,正则回调替换(未设置者替换为空)。AJAX 新楼层片段不经 page.before_render,由 reply.after_save 记录作者 uid 后在 content_after 内就地单作者渲染(单行一次查询,可接受)。

    钩子链陷阱(1.0.6 修复):topic.content_after 是管道追加语义($value 为先序插件已追加的内容,必须拼接返回),且核心 hook() 值替换 + 插件按 id 排序执行——id 较小的插件输出会被后续"整体替换"型消费者清掉(本插件曾被 topic_manage 旧版清掉主贴签名占位符)。挂接该钩子的插件一律 $value . 追加内容 返回。

    5.6 目标保护双通道

    • 渲染层(红区):levels_row_can_target($row) 用行内字段(经 attach_users 预载)+ group_by_id(settings 缓存)判定,零 DB;
    • 写路径:levels_can_target($uid) 查库复审——即便渲染层被绕过(如直接 POST),操作仍被拒绝;
    • 两通道共用 levels_row_uid() 归一提取用户 ID:帖子/回帖行用 user_id,用户行(me()/app_users)用 id——这是 1.0.1 修复的关键缺陷(两类行结构不同,直接读 user_id 会恒得 0 导致全部操作被拒)。

    5.7 用户名联想与理由选择(1.0.1 / 1.1.1)

    • 后台成员添加:data-levels-user-input 输入防抖 200ms → levels_search_users(need_admin + _csrf,前缀 LIKE 转义 / 纯数字按 uid 精确)→ fixed 定位下拉(#id + 用户名),↑↓/Enter/Esc/外点交互,参照 topic_manage 成熟实现;
    • 理由选择(1.1.1 / 1.1.2 / 1.2.0,topic_manage 同构):预设理由 select +「自定义输入…」选项——选预设由 JS 回填隐藏字段 reason;选自定义经统一的 reasonCustomVisible() 显示输入框(必填、自动聚焦)并实时同步,显隐同时切换输入框自身的 hidden 与其包裹 label(前台弹窗 label>input 形态、后台裸 input 形态都覆盖——只切 label 会踩 1.1.2 修复的后台卡死缺陷)。三处统一:后台成员添加(入组类预设)、后台成员移出(解除类预设)、前台快捷操作弹窗(选项由 JS 按 op 后缀 _clear/_remove 从 data-levels-reasons JSON 重建)。预设理由后台「功能设置」可配(两组 textarea 每行一条,levels_preset_lines() 归一化:trim、cut 60、去重、最多 30 条,空集回退 levels_preset_defaults() 内置默认)——一处配置三处生效。服务端无 JS 兜底:隐藏 reason 为
    #1
  • 123456789

    太复杂了🙃

    #2
  • one

    就是有点复杂,首个版本就1900行。
    迭代差不多40个版本,修修补补已经快2500行了

    #3
  • bbs1org

    这其实是用户组功能增强;你可以参考 https://addon.dismall.com/library/admcp/user/user_usergroups.html

    #4
  • one

    对,但我想相对轻量点。即不要DZ那么复杂,又能适配BBS1的轻量化。

    #5
  • bbs1org

    dz的也不复杂啊

    #7
  • one

    臃肿了。我就是用了多年的DZ,累

    #8
  • bbs1org

    使用有点复杂。但是代码不复杂。

    #9
  • one

    主要原因是积分还够,还有 DZ 那一套权限系统有点落伍了。
    如果我全部照搬的话,那不如用 DZ 了😉

    #10
  • bbs1org

    Discuz 从「单用户组」到「主用户组 + 多扩展用户组」为什么要加

    早期 Discuz(CDB / Discuz! 2.x/3.x 时代):一个用户只能归属 1 个用户组(groupid)。
    后来 X 系列引入 extgroupids 扩展用户组字段,支持一个用户同时挂多个用户组,主用户组决定基础身份,扩展用户组做权限叠加。

    数据库层面:pre_common_member

    • groupid:主用户组(唯一)
    • extgroupids:逗号分隔,存放多个附加用户组 ID,实现多组身份。

    为什么必须增加多用户组(扩展用户组)

    1. 业务身份出现交叉,单用户组无法表达

    单用户组的痛点:用户身份互斥,只能二选一。
    举例真实站长场景:

    • 某人是 A 板块版主(主组 = 版主),同时购买了全站 VIP;单用户组下:要么变成 VIP,丢失版主权限;要么保留版主,拿不到 VIP 权限。
    • 某个老会员,同时又是活动嘉宾、特殊认证用户。
    • 付费 VIP 到期,积分又很高,不能直接把他打回普通会员。

    如果只用单用户组,只能:

    1. 不断新建复合用户组(版主‑VIP、版主‑嘉宾……),组合爆炸,后台配置爆炸;
    2. 改权限复制粘贴,极易出错。

    多扩展用户组 = 权限乐高:主身份保留,叠加附加权限,不用创建大量复合组。

    2. 区分「积分晋级主组」和「人工 / 付费授予的临时特殊组」

    • 主用户组:跟随积分自动升降级(普通会员→中级→高级……),系统自动变更,由发帖、回帖积分驱动。
    • 扩展用户组:人工授予、付费购买、带有效期(VIP、嘉宾、特殊认证),不受积分升降影响。

    如果没有扩展组:用户积分涨上去,系统自动升级主用户组,就会把手工给的 VIP 身份冲掉,VIP 直接失效,大量投诉。
    这是早期版本非常致命的 bug 类痛点。

    3. 商业化运营需求:付费会员、限时特权

    2005‑2008 年大量论坛开始做收费 VIP:VIP 是附加特权,不是替换原有等级。

    • 用户本来是高级会员,花钱买 VIP,只是多一堆权限,不应该把他的积分等级覆盖掉。
    • VIP 到期,只需要移除这个扩展用户组,主用户组保持不变。

    单用户组很难做时效会员:到期要记住恢复旧用户组,要额外存一份旧 groupid,业务逻辑写得很丑陋;扩展用户组原生支持过期清除,逻辑干净。

    4. 版块权限精细化

    Discuz 的版块权限,允许给用户组设置:看版、发帖、下载附件权限。

    • 版块 A 只允许【版主】;版块 B 只允许【VIP】;
    • 用户同时需要两个版块权限,单用户组做不到;扩展用户组叠加后,同时拥有两套权限集合。

    5. 插件生态爆发,需要灵活的权限载体

    大量第三方插件:勋章、活动、任务、认证、付费购买用户组,都需要给用户追加一套权限标签。
    如果只能改主用户组,插件一运行就会破坏用户积分等级;扩展用户组作为附加槽位,插件只操作extgroupids,不碰groupid,插件之间互不打架。

    关键设计取舍:不是全部平等多组,而是「主组 + 若干扩展组」

    Discuz 没有做成完全平等的多角色,保留唯一主用户组:

    1. 用户前台显示头衔、星星、图标,以主用户组为准,避免多个头衔混乱;
    2. 积分自动晋级逻辑只作用在主用户组;
    3. 权限合并:主用户组权限 ∪ 全部扩展用户组权限。

    这是权衡:既解决权限叠加,又不破坏原有成熟的积分升级体系,老插件、老数据兼容。

    反面:如果坚持只用单用户组怎么实现同样需求?

    1. 用户表增加字段保存备用用户组;VIP 到期手工回填;
    2. 大量创建组合用户组(版主‑VIP、高级‑VIP);
    3. 插件自己维护一套独立权限标记,脱离用户组体系。

    缺点:逻辑分散、数据不一致、配置爆炸、第三方插件标准不统一。


    一句话总结

    早期论坛简单:每个人只有一个身份,用单用户组够用。
    当论坛出现:版主同时是 VIP、付费限时特权、积分自动升级不能冲掉手工身份、跨版块复合权限,互斥的单用户组模型就扛不住,于是增加扩展用户组做权限叠加,保留主用户组维持积分等级与前台显示。

    #11
  • one

    大可不必 用一个勋章就可以解决这个问题

    #13
  • one

    一、模型定性:两套体系解决的是不同问题

    Discuz!本插件(levels)
    用户组的本质权限容器:主组+扩展组可叠加,几十项权限按组勾选状态标记:六组固定、单一状态链互斥
    等级会员组按积分区间定级(组=等级)等级是积分纯函数,与组解耦,从不落库
    管理组三级职务(版主/超版/管理员)+ 自定义组挂靠,20+ 项细分权限全局 can_manage 一刀切 + 管理员互操作开关

    这个差异是本插件的刻意设计(单一状态链杜绝“既禁言又停用”的叠加态;等级纯函数零迁移),不建议照搬 Discuz 的多组叠加模型。对比的意义在于吸收其运营细节,而非推翻结构。

    二、本插件已优于 Discuz 的点(保持即可)

    • 公开管理日志:Discuz 管理日志仅管理员可见,本插件可对全员公开、自然语言事件句;
    • 处罚纪律:理由必填 + 预置理由归一 + 每次变更一行审计 + CSV 导出;Discuz 无强制理由;
    • 状态恢复链:prev_flag 承接 + 互斥顶替,杜绝标志位残留;Discuz 禁言/禁访可叠加且各自恢复逻辑独立;
    • 目标保护:uid=1 绝对保护、管理员互操作开关、渲染层/写路径双通道复审。

    三、值得借鉴的改进点(按优先级)

    P0 —— 体验闭环(改动小、价值高)

    1. 处罚告知:让被处罚用户知道“为什么、到什么时候”(借鉴 Discuz 处罚必有告知)

    • 现状差距:被禁言/禁访用户只看到「账号已停用中」或核心硬编码文案,不知道原因和到期时间——这是当前最大的体验缺口(管理员在成员列表能看到,本人看不到)。
    • 建议:levels_page_render 替换回帖框文案时,从 levels_members_of()(快照含 e 到期时间)取期限;理由可扩展镜像结构(members 增存 r 字段,写路径 levels_cache_rebuild 顺带写入,红区仍零 DB);/levels 页为当前用户展示处罚状态行。改动量:小。

    2. 处罚/解除的站内通知(借鉴 Discuz 警告通知作者)

    • 现状差距:levels_member_set/levels_member_remove 只写审计,不通知本人;管理员可能忘了转达。
    • 建议:复用 create_notification()(本插件升级通知已在用),设置页加开关(默认开),通知文案含组名、理由、期限。写路径一次插入,无红区问题。改动量:小。

    3. 禁访支持期限(借鉴 Discuz「禁止访问」可设有效期)

    • 现状差距:仅禁言有 expires_at,禁访(banned)只能永久,管理员被迫用“先禁访再手动解”。
    • 建议:expires_at 推广到 banned;到期惰性清理扩展 levels_expire_mutes() 为通用到期清扫(现有 60 秒原子节流 + rowCount 认领模式直接复用);前台快捷操作禁访键弹窗出现时长档位。改动量:中,表结构无需变更(expires_at 列已存在,语义放宽)。

    P1 —— 权限精细化

    4. 按等级差异化发帖频率(借鉴 Discuz 组级「每小时发帖数」)

    • 核心已有现成钩子 security.rate_allow(放行判断,返回 false 触发限制)——本插件可挂接,按当前用户等级给出差异化阈值(如 LV0 每小时 2 帖、LV3 以上不限)。O(1) 内存判定,不碰红区。前置查证:实施前需核实该钩子 ctx(ip/bucket)与核心各 bucket 的触发点语义。改动量:中。

    5. 个性签名分级(借鉴 Discuz「允许自定义头衔」组权限 → 改为等级门槛更贴合本插件哲学)

    • 建议:签名增加「最低等级」设置(如 LV2 起可用),或上限随等级增长;纯内存判定,与门槛体系同构。改动量:小。

    6. 前台快捷操作的权限口径对齐核心

    • 现状差距:快捷操作用全局 can_manage();核心另有 can_manage_topic($t)(主题操作已在用)。若核心未来引入版主/所辖版块概念,本插件应跟随核心函数而非自建分层——先查证两个函数的语义差异再决定,避免臆造。管理职务三级分层属于核心权限模型,插件不宜绕过核心自建,可经 levels.query 扩展 API 暴露状态供未来演进。

    P2 —— 运营效率

    7. 批量成员操作(借鉴 Discuz 批量编辑):成员列表多选批量移出 / 粘贴多用户名批量入组;逐用户事务写入(写路径允许),注意理由统一、失败逐条反馈。
    8. 禁言时长档位后台可配:levels_mute_durations() 现硬编码 7 档,可仿预置理由做成配置(归一化:正整数秒、去重、≤20 档)。
    9. 处罚概况卡片:后台成员管理页顶部展示各组当前人数(从 JSON 镜像 O(1) 统计),一眼掌握全站处罚状态。

    长期可选(需慎重)

    10. 组定义数据化(后台自定义组):Discuz 任意自定义组是它的核心卖点,但本插件六组固定键深度耦合状态链优先级、互斥集合、prev_flag 语义。若确有「VIP」「内测组」类需求,建议先评估:新增组是否只是「特殊组」的别名(用组名自定义即可部分满足——组名/颜色本就可配)。在需求明确前不动,避免为灵活性破坏单一状态链的确定性。

    四、明确不建议做成 Discuz 机制

    • 多组叠加(主组+扩展组):破坏单一状态链,收益低、复杂度高;
    • 收费公众组/交易积分体系:bbs1 无交易积分基础设施;
    • HTML 代码权限:Discuz 文档自己都警告安全隐患,与本插件「Markdown 渲染自带 XSS 防护」的边界冲突;
    • 几十项权限 checkbox 大爆炸:与极简哲学冲突,本插件的「等级纯函数 + 六组状态」用 1/10 的配置面覆盖了小站实际需要的 80% 场景。
    #6
  • flinthub

    你迭代几轮,代码行数可以跟bbs1org看齐了😄

    #12
  • one

    干不动啊

    #14
  • dcve

    头疼,内核一更新,整个插件都需要更改,我还在用8.6.0版本。。。

    #15
  • zaiwai

    兄弟,确实挺复杂的。我都看不动。😇

    #16
  • 9

    看起来很厉害的样子

    #17
  • NB

    这种等级没啥意思了,现在流行Discourse的信任等级。

    #18
  • bbs1org

    分享下Discourse的信任等级设计方案

    #19
  • george

    很牛逼很牛逼

    #20

发表回复

登录后回复