DS读文

插件系统与开发指南

Clara 插件体系完整规范:目录结构、plugin.php 写法、生命周期、169 个钩子点全景、内置插件说明与开发指南。

06 · 插件系统

核心实现:[app/Core/Plugin.php](../app/Core/Plugin.php) + [app/Core/Hook.php](../app/Core/Hook.php)。插件目录:[content/plugins/](../content/plugins/)。

系统共预留 164 个钩子(61 个业务事件 fire + 103 个数据/视图插槽 filter;2026-10-01 源码普查校准),按四层体系组织:

层类型数量用途
业务事件层Hook::fire(action)61写操作完成后通知插件(发帖/回帖/交易/签到/后台管理……),只通知不改数据
数据管线层Hook::filter6数据流经插件链式加工(正文渲染/页面标题/通知链接/头像 HTML/货币类型名/首页轮询指纹)
前台视图插槽层Hook::filter82在前台各页面位置注入 HTML
后台视图插槽层Hook::filter15在后台页面位置注入 HTML

1. 插件规范

content/plugins/{目录名}/          # 目录名:字母数字开头,可含 - _,≤50 字符
├── plugin.php                    # 主文件(必需)
├── install.php                   # 启用时执行(建表/初始化,可选;需幂等——禁用后再启用会重复执行)
├── uninstall.php                 # 卸载前执行(清表,可选)
└── views/                        # 插件视图(View::renderFile 渲染,可选)

plugin.php 结构:

<?php
/* 防直接 HTTP 访问(必须) */
if (!defined('BASE_PATH')) {
    exit;
}

/*
Plugin Name: 插件名称
Plugin URI: https://example.com
Description: 插件描述
Version: 1.0.0
Author: 作者
*/

/* 在此注册钩子(文件被 include 时执行) */
Hook::on('thread_created', function ($data) {
    // ...
});

/* 文件末尾 return 配置声明(后台「插件中心 → 配置」自动渲染表单) */
return array(
    'config' => array(
        'some_key' => array(
            'label' => '配置项名',
            'type'  => 'text',           // text / textarea / select 等
            'default' => '默认值',
            // 'options' => array(...)   // select 用
        ),
    ),
    // 'admin_url' => 'ai-reply',        // 可选:声明后「配置」按钮直达该前台路由
);

要点:

  • 元信息解析(Plugin::parseMeta):扫描全部块注释,取含 Plugin Name 的那个;行首 * 可有可无(兼容 WordPress 格式);键名小写下划线化。
  • 防重复 include(Plugin::loadFile 的 $loadedFiles 请求级缓存):boot 与 configDef 共用一次 include,防止插件内全局函数重复声明致命错误——在插件里声明全局函数是安全的。
  • 数据约定:自建表必须以 plugin_{目录名}_ 为前缀;配置读写用 Plugin::config($dir, $defaults) / Plugin::saveConfig($dir, $data)(存 settings 表 plugin_config_{目录} 键)。

2. 插件生命周期

操作入口行为
启用后台插件中心 → Plugin::activate($dir)执行 install.php → 写 enabled_plugins → 写 installed_plugins(此后显示卸载按钮)→ 初始化默认配置(已有不覆盖)
禁用Plugin::deactivate($dir)仅移出启用列表;数据/配置保留,重新启用即恢复
卸载Plugin::uninstall($dir)先禁用 → 移除 installed 标记 → 执行 uninstall.php(清插件表)→ 删除配置键;插件文件目录保留(手动删除)
每请求bootstrap → Plugin::boot()include 所有已启用插件的 plugin.php(注册钩子);目录被删则跳过

安全边界:Plugin::all() 列表扫描只读源码不执行——未启用插件代码永不运行;boot/loadFile 异常全部捕获写 storage/logs/plugin_error_Ymd.log。

3. 钩子点全景(164 个 · 2026-10-01 源码普查校准)

3.0 钩子机制

Hook::on($name, $callback, $priority = 10);   // 注册(Hook::register 为别名)
Hook::fire($name, $data);                     // action:依次执行回调,忽略返回值,返回原始 $data
Hook::filter($name, $value, $data = null);    // filter:链式加工,返回最终值
Hook::has($name); Hook::clear($name);         // 查询 / 清空(测试用)
  • 优先级:第三参 $priority 越小越先执行(默认 10),同优先级按注册顺序。
  • 异常隔离:所有回调在 try/catch 中执行,插件抛错只记 storage/logs/plugin_error_Ymd.log,不影响主流程。
  • filter 回调签名:function ($value, $data);核心未传上下文时回调只收到 $value($data 参数务必给默认值,如 function ($html, $thread = null))。
  • fire 回调签名:function ($data),$data 为关联数组。

3.1 业务事件层(action,61 个)

系统与路由(2)

钩子触发点载荷($data)现有消费者
routes_registerbootstrap.php,前台路由加载后$routerrank(注册 /rank 页)、api\_hub
admin_routes_registeradmin\_routes.php,后台路由加载后、通配之前$routerai\_reply、api\_hub、ai\_writer(各注册自管面板路由)

账户与资料(9)

钩子触发点载荷($data)现有消费者
user_registered注册成功array(id, username, email)—
user_login登录成功(含记住我自动登录、2FA 二次验证)array(user, remember)—
user_login_failed登录失败(密码错误/账号锁定等)array(login, ip)—
user_logout退出登录array(user)—
profile_updated用户中心保存资料array(user_id, data)—
avatar_updated用户中心上传新头像array(user_id, url)—
password_changed修改密码成功array(user_id)—
user_followed关注用户array(user_id, target_id)—
user_unfollowed取消关注array(user_id, target_id)—

主题与互动(14)

钩子触发点载荷($data)现有消费者
thread_created发帖成功(前台发帖 / 后台发布 / ai\_writer 代发,三处均触发)前台:array(id, user_id, forum_id, title);后台与 ai\_writer 额外携带 thread_id(同 id)、contentai\_reply(主题级回复)、rank(活跃分)
thread_updated主题编辑保存后array(thread 合并后数据, editor_id)—
thread_deleted主题删除:前台软删(仅状态实际变化时一次);后台删除带 permanent=truearray(thread, operator_id[, permanent])—
thread_moderated版务操作(置顶/加精/关闭/隐藏)后array(thread, field, column, value, operator_id)—
thread_liked点赞/取消点赞主题(两个状态共用)array(thread_id, user_id, liked)—
thread_favorited收藏/取消收藏主题array(thread_id, user_id, favorited)—
post_created回帖成功(含 AI 回帖)array(id, thread_id, user_id, content)ai\_reply(楼层级回应)、rank
post_deleted回复删除后array(post, operator_id)—
post_liked楼层点赞/取消array(post_id, thread_id, user_id, liked)—
poll_voted投票提交成功array(poll_id, thread_id, user_id, option_ids)—
bounty_accepted悬赏采纳最佳答案array(thread, post, amount, currency_id, operator_id)—
bounty_refunded取消悬赏、托管款退回array(thread, amount, currency_id, operator_id)—
thread_purchased购买付费主题成功array(thread, buyer_id, price, currency_id)—
thread_viewed帖子被浏览(与 views 计数同语义:会话去重 1 小时后触发,插件无需自行防刷新;游客 user\_id=0;v2026.09.06)array(thread_id, user_id, forum_id)—

附件(2)

钩子触发点载荷($data)现有消费者
attachment_uploaded上传附件:type=file 文件模式与 type=link 网盘模式共用同一钩子名,插件按 type 分流array(attachment_id, type, user_id, url, price)—
attachment_bought付费附件购买成功array(attachment, buyer_id, price, currency_id)—

财富与交易(5)

钩子触发点载荷($data)现有消费者
currency_changed全站货币变动统一出口(currency\_change() 内部,覆盖签到/悬赏/附件/商城/卡密/邀请等全部记账点;事务提交后触发——监听器异常不影响已落账数据,也不可再回滚余额)array(user_id, currency_id, amount, type, note, balance)—
gift_sent送礼成功(帖内礼物与用户礼物共用)array(from_user_id, to_user_id, thread_id, gift, price)—
member_purchased购买会员成功array(membership, user_id, price)—
frame_purchased购买头像框成功array(frame, user_id, price, expire_at)—
card_redeemed充值卡兑换成功array(card, user_id)—

签到(2)

钩子触发点载荷($data)现有消费者
checkin_done每日签到成功array(user_id, streak, total)rank
checkin_repaired补签成功array(user_id, date, cost, streak)—

通知与私信(3)

钩子触发点载荷($data)现有消费者
notification_sentnotify() 发送通知(合并进已有未读时 merged=true,新发为 false)array(uid, type, title, content, link, merged)—
pm_sent私信投递成功(新会话与会话内回复共用 deliver 统一出口)array(conv_id, from_id, to_id)—
pm_deleted私信会话删除(仅本人侧隐藏,对方不受影响;v2026.09.06)array(conv, user_id)—

设计边界:通知中心的已读/删除(NotificationController::read/delete)不设事件——纯用户对本人通知的管理操作,无对外语义,插件监听无实际价值;如需通知联动,监听 notification_sent 即可。

后台管理(18)

钩子触发点载荷($data)现有消费者
user_edited后台编辑用户保存array(user_id, old, new, operator_id)—
user_muted后台禁言用户array(user_id, until, reason, operator_id)—
user_unmuted后台解除禁言array(user_id, operator_id)—
user_banned后台封禁账号array(user_id, operator_id)—
user_unbanned后台解封账号array(user_id, operator_id)—
user_group_changed后台变更用户组array(user_id, old_group_id, group_id, operator_id)—
forum_saved后台保存版块(新建/编辑)array(forum_id, data, operator_id)—
forum_deleted后台删除版块array(forum, operator_id)—
group_saved后台保存用户组array(group_id, data, operator_id)—
group_deleted后台删除用户组array(group, operator_id)—
ip_ban_added后台新增 IP 封禁array(ip, reason, expires_at, operator_id)—
ip_ban_updated后台更新 IP 封禁配置array(ip, reason, expires_at, operator_id)—
ip_ban_removed后台解封 IParray(ip, operator_id)—
settings_saved后台系统设置保存(含分组名与该组键列表)array(tab, keys, operator_id)—
mail_batch_sent后台邮件群发完成array(subject, to, ok, fail, operator_id)—
admin_threads_batch后台帖子批量操作(删除/隐藏/恢复/移动等)array(act, ids, affected, operator_id)—
report_submitted用户提交举报(前台)array(type, target_id, reporter_id, reason)—
report_handled后台举报处理完成array(report, operator_id)—

插件生命周期(3)

钩子触发点载荷($data)现有消费者
plugin_activated插件启用成功array(dir, operator_id)—
plugin_deactivated插件禁用array(dir, operator_id)—
plugin_uninstalled插件卸载完成array(dir, operator_id)—

版块关注与举报(3,v2026.09-10 补录)

钩子触发点载荷($data)现有消费者
forum_followedForumController@followToggle 关注成功array('user_id', 'forum_id')关注通知类插件
forum_unfollowedForumController@followToggle 取消关注array('user_id', 'forum_id')同
report_clearedAdminReportsController@clearPost 批量清空举报array('status', 'count', 'operator_id')举报清理联动插件

3.2 数据管线层(filter,6 个)

核心数据流的统一出口,回调 function ($value, $data),必须 return 加工后的值。

钩子位置$value上下文 $data典型用途
content_formathelpers.php md_render() 返回处(所有帖文/签名/个人简介渲染的唯一出口)渲染后的 HTMLarray('raw' => 原始文本)插件自定义标签(如 [mytag]…[/mytag])、内容水印、外链转跳转页
page_titlelayout.php 页面标题当前页面标题串不传按页面动态追加标题后缀
notification_linkhelpers.php notify() 入口通知跳转链接array(uid, type, title)改写通知落地页(如积分任务通知跳插件页)
avatar_htmlhelpers.php avatar_html()头像 <img> HTMLarray(user, size)头像挂件、VIP 边框
currency_type_namehelpers.php currency_type_name()流水类型中文名不传(返回值匹配)注册插件货币流水类型的中文显示名
home_sync_sigHomeController 首页轮询接口 /home/sync 指纹轮询指纹串不传并入插件状态指纹,驱动已打开的首页自动刷新(仅首页有此钩子)
/* 示例:自定义内容标签(在 md_render 之后、输出之前生效) */
Hook::on('content_format', function ($html, $data = null) {
    return str_replace('[hr]', '<hr class="my-hr">', $html);
});

3.3 前台视图插槽层(filter,82 个)

回调接收并返回 HTML 串(视图用 <?= Hook::filter(...) ?> 直接输出)。输出前必须自行 e() 转义来源数据。

全站布局(layout.php,15 个)

钩子位置上下文 $data典型用途
layout_head</head> 前—注入 CSS/JS/meta(每页生效)
layout_body_start<body> 顶部—全站浮层/顶部公告条
layout_body_end</body> 前(晚于核心脚本)—底部 DOM/脚本
nav_links顶部导航—加导航链接(用 class="nav-link" 保持样式)
nav_logo_after品牌 Logo/站名链接后—站点徽标/内测标注(只能放非交互内联元素,禁嵌套链接)
nav_tools_before顶栏工具区前(搜索/通知/头像之前)—顶栏工具类入口
nav_search_after顶栏搜索框后—搜索增强/快捷入口
user_menu_links用户下拉菜单array(user)我的订单/我的商店等菜单项
main_area_before / main_area_after主内容区顶/底—全站横幅;三栏主题左栏挂载点
footer_links页脚—页脚链接
side_nav_links / side_nav_bottom左侧吸附导航(后台开侧导航时)—侧导航链接与底部卡片(nav_links 原样并入)
lq_links / lq_bottom左侧快捷导航卡(partials/left-quick-nav.php)—快捷导航卡专用追加位(与顶部导航共用 nav_menu_* 开关)

三栏主题接入模式:核心不提供固定左栏 DOM,由主题插件组合实现——main_area_before 输出 <div class="plugin-left-col"> + layout_head 注入栅格 CSS(把 .main-area 改为 grid 三栏/两栏)+ layout_body_end 注入交互 JS。改核心布局零侵入,多主题插件可共存按启用互斥。

登录 / 注册(6 个)

钩子位置上下文 $data
login_form_before / login_form_after登录页顶/底—
login_form_fields登录表单内附加字段—
register_form_before / register_form_after注册页顶/底—
register_form_fields注册表单内附加字段—

首页(home.php,5 个)

钩子位置上下文 $data
home_content_before首页主体顶部—
home_hero_after统计横幅之下、版块列表之上—
home_left_top / home_left_bottom三栏布局左栏顶/底—
home_main_bottom三栏布局中栏底—

侧栏(首页/版块/帖子三处共用 + 语义化卡片锚点,12 个)

钩子上下文 $data
sidebar_top / sidebar_mid / sidebar_bottom首页/版块/帖子三处侧栏顶/中/底(mid = 首页热门主题后 / 版块页版块信息卡后 / 主题页作者卡后);home.php 不带上下文;forum.php 传 $forum;thread.php 传 $thread
sidebar_hot_after / sidebar_newuser_after / sidebar_friendlink_after首页语义化卡片锚点(2026-09-10 补全,对齐 XiunoX「每张原生卡片后留缝」):热门主题卡后 / 新成员卡后 / 友链卡后
sidebar_foruminfo_after / sidebar_mods_after版块页语义化锚点:版块信息卡后 / 版主卡后(无版主不渲染)
sidebar_author_after / sidebar_related_after / sidebar_latest_after / sidebar_forummini_after帖子页语义化锚点:作者卡后 / 相关主题卡后(无相关帖不渲染)/ 本版最新回复卡后 / 版块迷你卡后

ctx 与所在页侧栏钩子一致;卡片被条件隐藏时对应缝不渲染。

版块页(forum.php,6 个)

钩子位置上下文 $data
forum_content_before / forum_content_after版块页顶/底(两种布局均生效)$forum
forum_threads_before主题列表卡内顶部array(forum, filter, cat_id)
forum_threads_after主题列表卡内底部array(forum, filter, threads)
forum_left_top / forum_left_bottom三栏布局左栏顶/底—

帖子详情页(thread.php,10 个)

钩子位置上下文 $data
thread_head_after标题/标签区与正文之间$thread
thread_content_before / thread_content_after正文前/后$thread(现有消费者:thread\_visitors)
thread_actions_extra主楼操作条内(点赞/收藏/分享按钮追加,与楼层 post_actions_extra 对称)$thread
post_author_extra每层楼作者名附加徽章$p 楼层行
post_actions_extra每层楼操作按钮区$p
post_floor_extra每层楼操作条下附加内容$p
reply_form_before回复表单前$thread
thread_left_top / thread_left_bottom三栏布局左栏顶/底—

发帖 / 编辑页(thread-new\.php + thread-edit.php 共用,2 个)

钩子位置上下文 $data
post_form_before / post_form_after表单上/下方发帖 array(forum);编辑 array(forum, thread)

编辑器(partials/editor.php,前后台共用,2 个)

钩子位置上下文 $data
editor_toolbar_buttons工具栏按钮区array(canUpload, editorId)
editor_after编辑器下方附加区array(canUpload, editorId)

列表行(partials/thread-row\.php,1 个)

钩子位置上下文 $data
thread_row_extra主题行内附加信息(首页/版块/用户主页列表共用)$t 主题行

用户主页(user.php,3 个)

钩子位置上下文 $data
user_profile_header_after主页头部之下$u
user_profile_tabstab 栏尾(链接用 ?tab=xxx#profile-tabs 防跳页顶)$u
user_profile_tab_contenttab 面板区首(插件自判 $data['tab'] 渲染面板)array(tab, user)

用户中心(uc.php,3 个)

钩子位置上下文 $data
uc_nav_links中心导航尾$me
uc_content_before内容区顶部(每页生效)array(tab, me)
uc_tab_content主区首(插件自判 $data['tab'] 渲染整页)array(tab, me)

搜索 / 标签 / 签到 / 商城 / 礼物页(8 个)

钩子位置上下文 $data
search_content_before / search_content_after搜索页顶/底array(q) / array(q, total)
tag_content_before / tag_content_after标签页顶/底array(tag)
checkin_content_before签到页顶部array(streak)
shop_content_before / shop_hero_after商城页顶部 / 横幅下—
gifts_content_before礼物墙页顶部—

私信页(pm-inbox.php + pm-view.php 共用,2 个;v2026.09.06)

钩子位置上下文 $data
pm_content_before / pm_content_after私信页顶/底(两页共用)收件箱不传;会话页传 array(conv, partner)

插件自定义 tab 页模式(uc_tab_content / user_profile_tab_content):插件注册路由渲染页面骨架,或在钩子里输出面板——内置 tab 为 if/elseif 链且无 else 兜底,插件 tab(如 shop)不命中任何内置分支,仅插件内容渲染;tab 链接用 ?tab=xxx(user 主页需带 #profile-tabs 锚点)。

应用中心(apps.php,2 个;v2026.09 补录)

钩子位置上下文 $data
app_cards应用中心功能卡片区(与 nav_links 同挂,结构化:icon/name/desc/url)$cards(数组)
apps_content_after应用中心页底部$html

专题页(topic.php / topics.php,5 个;v2026.09 补录)

钩子位置上下文 $data
topic_content_before / topic_content_after专题页顶/底(详情与总览共用,载荷含 null 兼容)array('topic' => $topic 或 null)
topic_main_bottom专题详情主区底部$html
topic_sidebar_top / topic_sidebar_bottom专题详情侧栏顶/底$html

3.4 后台视图插槽层(filter,15 个)

回调接收并返回 HTML 串,仅在后台页面生效;后台只加载 admin.css(样式需自带 <style> 内联或经 admin_head 注入,不能依赖 app.css)。

布局(admin/layout.php,6 个)

钩子位置上下文 $data
admin_head</head> 前—
admin_body_start<body> 起始—
admin_body_end</body> 前(晚于核心脚本)—
admin_nav_links侧栏菜单尾(自组 .nav-group 结构)array(module) 当前模块
admin_content_before / admin_content_after内容区顶/底(每页生效)array(module)

注意:核心侧栏菜单按多级管理组模块授权自动过滤(Auth::adminModules()),但插件经 admin_nav_links 输出的菜单项不会被自动过滤——插件需自行调用 Auth::adminModules() 判断(返回 null=全权限,否则为模块名数组)再决定是否输出。

各管理页(9 个)

钩子位置上下文 $data
admin_dashboard_stats_after仪表盘统计卡下方array(stats)
admin_dashboard_content_after仪表盘页尾array(stats, env)
admin_settings_tabs系统设置页 tab 栏尾array(tab)
admin_settings_panels系统设置页面板区(配套 tabs 加的页签)array(tab)
admin_user_edit_sections用户编辑页底部区块$u
admin_group_form_after用户组编辑页表单下方$group(新建时 null)
admin_thread_row_actions帖子列表行操作区$t 行数据
admin_thread_form_after帖子发布/编辑页表单下方array(context: new/edit[, thread])
admin_tools_items_before系统工具列表前置项(自组 .tool-item 表单)—

集成系统设置页模式(admin_settings_tabs + admin_settings_panels 配对使用):

/* 1. tab 栏尾加页签链接 */
Hook::on('admin_settings_tabs', function ($html, $data = null) {
    return $html . '<a class="tab-btn ' . (($data['tab'] ?? '') === 'myplug' ? 'is-active' : '') . '"'
        . ' href="' . aurl('settings?tab=myplug') . '">我的插件</a>';
});

/* 2. 对应 tab 渲染面板 */
Hook::on('admin_settings_panels', function ($html, $data = null) {
    if (($data['tab'] ?? '') !== 'myplug') {
        return $html;   /* 非本插件 tab 直接返回 */
    }
    return $html . '<form method="post" action="' . url('myplug-save.html') . '">'
        . csrf_field() . ' /* 表单字段 */ </form>';
});

关键约束:面板表单必须提交到插件自有路由(经 routes_register 注册)——核心 settings/save 端点有 $keys[$tab] 白名单,只接受内置 tab 的固定键名,插件的 POST 字段会被丢弃。自有路由内自行做 Auth::isAdmin() + Csrf::check() 后 Plugin::saveConfig()。

4. 内置插件(17 个)

系统内置 17 个官方插件全部随包交付、装完即启用,覆盖社区运营的主要进阶场景——它们全部以标准插件形态实现、核心代码零改动,本身就是「插件能承载完整功能」的活证。功能入口见前台「应用中心」。

插件实现细节(内部机制、数据表结构、调度与并发策略等)不在公开文档范围内。插件开发所需的规范、钩子契约与公共设施 API 见上方章节——按这些公开契约开发,能力与官方插件完全对等。

插件功能
ai_replyAI 智能回复:多提供商自动回帖
site_rank社区排行榜:热门主题 / 活跃用户 / 财富榜
thread_visitors帖子访客:最近浏览会员展示
ai_writerAI 智能写作:标题池 + 文章生成 + 定时发布
api_hubAPI 工具箱:多平台解析 + 按次计费
member_wall首页会员墙:推荐 / 最新 / 活跃 / 财富四榜
ad_self广告位自助投放:按周期购买广告位
lottery幸运抽奖:回帖抽奖 + 概率抽奖 + 转盘
ks_exam入站考试:通过考试解锁互动权限
geo_qa_factoryGEO 问答工厂:AI 问答对生产与发布
brand_landing品牌落地页:站点介绍全屏页
quick_reply快速回帖:楼层一键短语回复
copy_tail复制保护:复制内容追加来源小尾巴
mourning黑白哀悼模式:全站灰度一键切换
pet_garden论坛萌宠:养成互动与萌宠榜
sync_platform外站同步:公众号 / CSDN 一键分发
geo_checkGEO 检测:单篇收录体检

5. 插件开发指南

5.1 最小可用插件

<?php
// content/plugins/hello/plugin.php
if (!defined('BASE_PATH')) {
    exit;
}

/*
Plugin Name: Hello
Description: 在帖子正文后追加一句问候
Version: 1.0.0
Author: Me
*/

Hook::on('thread_content_after', function ($html, $thread = null) {
    return $html . '<div class="hello-box">感谢阅读「' . e($thread['title'] ?? '') . '」</div>';
});

启用:后台「插件中心 → 启用」即生效(无需重启,每请求 boot 加载)。

5.2 带配置与自建表

  • install.php 中用 Db::q() 建表(表名 Db::t('plugin_hello_items'),自动带前缀);uninstall.php 中 DROP TABLE。
  • plugin.php 末尾 return array('config' => array(...)) 声明配置 → 插件中心出现「配置」按钮(AdminPluginsController::settings 通用表单)。按钮仅启用后显示,配置端点校验启用状态(未启用插件的主文件不得被 include 执行)。
  • 读配置:Plugin::config('hello', array('k' => '默认值'))。

5.3 自定义后台面板

return array(
    'admin_url' => 'hello-admin',   // 「配置」按钮直达 url('admin.php/hello-admin')
);

Hook::on('admin_routes_register', function ($router) {
    $router->any('/admin.php/hello-admin', function () {
        Auth::boot();
        if (!Auth::isAdmin()) {
            abort_page(403, '无权限');
        }
        if (Request::isPost()) {
            Csrf::check();
            // 保存配置 Plugin::saveConfig('hello', $_POST)
        }
        $content = View::renderFile(BASE_PATH . '/content/plugins/hello/views/admin.php', array(
            'cfg' => Plugin::config('hello'),
        ));
        echo View::render('admin/layout', array('pageTitle' => 'Hello 设置', 'content' => $content));
    });
});

注意:插件后台路由必须自行做 Auth::isAdmin() + Csrf::check()(核心守卫不覆盖插件路由);且必须注册在 admin_routes_register 钩子内(先于通配路由匹配)。

5.4 核心公共设施:Cache 缓存与 Cron 计划任务(v2026.09.06)

插件需要的两类通用能力已抽入核心,不要再自造轮子(参照 member_wall 迁移:删掉自有的 mw_cache_get/set/purge 约 30 行,改 Cache::remember 一行式,行为不变)。

缓存([app/Core/Cache.php](../app/Core/Cache.php))——榜单、热点数据、外部接口响应等:

/* 读缓存,未命中查库并回写 5 分钟(最常用) */
$rows = Cache::remember('mw_rank_new_0', 300, function () {
    return Db::all('SELECT ...');
});

/* 配置变更时主动失效(不等 TTL 自然过期) */
Cache::forget('mw_rank_new_0');
  • 键约定:加插件前缀防冲突(如 mw_、aw_);只含 [A-Za-z0-9_.-](其余字符自动转下划线)。
  • remember 的空值也算命中(防空穿透:查询失败返回空数组也被缓存,不会每次打穿到库);需区分「未命中」用 Cache::get($key) === null。
  • 后台「系统工具 → 清理缓存」会一并清掉(*.cache 后缀约定)。

计划任务([app/Core/Cron.php](../app/Core/Cron.php))——定时刷新、清理、汇总等:

/* plugin.php 顶层注册(boot 阶段即生效) */
Cron::register('member_wall/refresh', '会员墙榜单刷新', 300, function () {
    foreach (array('new', 'active', 'rich') as $t) {
        Cache::forget('mw_rank_' . $t . '_0');
    }
    return '已刷新 3 个榜单';   /* 返回摘要,后台「系统工具→计划任务」展示 */
});
  • 任务名全局唯一,建议 插件目录/功能;间隔 ≥60 秒。
  • 触发零配置:前台有访问即自动驱动(懒触发);低流量站可在服务器配置 cron 每分钟请求 /cron.html?key=密钥 精准触发(密钥在后台系统工具页)。
  • 任务体要求幂等(锁超时或进程被杀后下次触发会重跑);异常自动记录为失败,不连坐其他任务。
  • 依赖 cron_runs 表(数据库升级建);未升级时任务静默跳过,站点不受影响。
  • 特例说明:ai_writer 因需要超出固定间隔模型的定制调度能力,保留自有调度不迁移;简单定时需求一律用核心 Cron。

常用核心函数速查

记账与通知(多货币与通知体系的唯一入口——勿直接写 SQL 改余额表/通知表,并发正确性与事件触发都在封装内):

/* 记账统一入口:$amount 可负(扣款);返回 bool(余额不足且非 $force 时失败)
   事务内行锁保证并发正确,成功后触发 currency_changed 事件 */
currency_change($uid, $cid, $amount, $type, $note = '', $force = false);

/* 发送站内通知(同语义重复通知自动合并未读) */
notify($uid, $type, $title, $content = '', $link = '');

**数据库(Db 封装,全部预处理参数化防注入;列名自动反引号,手写 SQL 避开 MySQL 保留字)**:

Db::t('threads');                    // 表名 → 带前缀完整表名
Db::q($sql, $params);                // 预处理执行(INSERT/UPDATE/DELETE/DDL),返回 PDOStatement
Db::all($sql, $params);              // 多行 → 二维数组
Db::one($sql, $params);              // 单行 → 关联数组(无结果 null)
Db::val($sql, $params);              // 单值(如 COUNT),无结果 null
Db::insert($table, $data);           // 关联数组插入
Db::update($table, $data, $where, $params);  // 更新($where 字符串条件)
Db::delete($table, $where, $params); // 删除
Db::count($table, $where, $params);  // 计数

权限与输出:

Auth::check();                       // 是否已登录
Auth::isAdmin();                     // 是否管理员(含全权限组)
Auth::can($perm);                    // 当前用户是否拥有某权限点
url('thread-123.html');              // 站内 URL(自动兼容子目录部署)
e($text);                            // HTML 转义——输出一切来源不明数据前必用

5.5 界面文案 i18n(v2026.09.15 起)

核心 i18n 机制详见 [02-核心框架.md](02-核心框架.md) Lang 章节(源串即键:zh-cn 直通,未收录回退中文永不空文案)。插件接入零门槛——照常写中文,输出层包 t():

/* PHP 侧(视图/控制器输出) */
echo t('已复制 ') . $n . t(' 条');
echo t('奖励 %s 金币', array($amount));   /* 占位符 sprintf 语法 */
/* JS 侧(fetch 回包后的提示等):查 window.CLARA_I18N,zh-cn 空表回退原文 */
claraToast(T('购买成功'));

两大禁忌(违者 fatal,生产事故沉淀):

  1. 编译期常量位置禁调 t()——static $x = array(...t()...) / const / 类属性默认值 / 参数默认值皆编译级 fatal。static 用惰性初始化(= null + if (=== null) 填充)
  2. plugin.php 顶级代码禁调 t()——加载期 setting() 未定义,t() → Lang::boot() 即 fatal。Cron::register 的 $label 传中文字面量(后台任务列表展示期自动 t() 翻译),其余顶级 t() 移入函数/闭包体内

补译(可选增强,不补也不破功能):新串在 content/lang/en.php + zh-tw.php 同键补条目(键 = 中文源串逐字符一致;占位符序列/换行数/首尾空格守恒);仅 JS 用的串另加入 content/lang/js-keys.php 清单(JS 翻译表按此清单裁剪注入,不整包内联)。

5.6 开发注意事项

  1. 插件 PHP 文件必须加 BASE_PATH 防护头,防直接 HTTP 访问。
  2. 页面级样式用内联 <style> 随输出渲染,类名统一加插件前缀(如 tv-),复用 app.css 的 CSS 变量(--bd/--soft-2/--tx-2/--radius-s/--ac)保证主题一致。后台插槽输出的内容不能依赖 app.css(后台只加载 admin.css)。
  3. 插件抛错会被 Hook 捕获记日志,但不要在回调里做致命假设(如未判空直接用 $thread['x'])。
  4. 钩子回调签名:filter 型为 function ($value, $data)($data 可能不传,需默认值);fire 型为 function ($data)。
  5. 多 tab 共用一个 save 端点的插件面板:保存时按表单 hidden 标识分支构造 $new,只更新本页字段,否则未提交字段会经 array_merge 默认值洗掉另一 tab 的配置。
  6. 监听 thread_created 时以 id 键为主(前台/后台/ai\_writer 三处触发均已对齐;thread_id/content 仅后台与 ai\_writer 额外携带,勿作必需字段依赖)。
  7. 动作型钩子(点赞/收藏等)携带状态布尔(liked/favorited),监听方需区分正向与取消操作,避免重复记账。
  8. 建表与手写 SQL 避开 MySQL 保留字:列名撞保留字(如 explain、order、group、key)会直接 1064 语法错误导致 install.php 中断(真实案例:ks_exam 首版 explain 列)。Db::insert/update 等封装会自动反引号列名,但手写 SQL(建表 DDL、SELECT 列清单)必须自己加反引号(` explain ),或干脆换列名(如 tip`)。

本文档随系统发布维护;如发现与当前版本不符,欢迎到社区反馈。