开发文档
Zerjie(择界)系统内核的完整开发参考——主题、插件、小工具、后台扩展、核心 API、安全组件
简介
Zerjie(择界)是一款基于 纯原生 PHP 开发的主题驱动型内容管理系统。核心理念是「一核载万态 · 主题定乾坤」——内核只提供基础设施,业务逻辑完全由主题定义。
关键特点:
- 零依赖:不依赖 Composer,不依赖任何 PHP 框架
- 轻量:核心代码 1MB 以内,单文件入口
- 三级扩展:主题 / 插件 / 小工具,互不耦合
- 安全内建:CSRF、XSS、SQL 注入防护、IP 限流、分级防御、完整性校验开箱即用
- 开箱即用:后台菜单、设置页、小工具管理、用户管理、SMTP、站点锁、CLI 命令全部内置
- 多驱动:缓存 / 日志 / 队列都支持多驱动,可自由扩展
扩展体系一览:
| 类型 | 位置 | 职责 |
|---|---|---|
| 主题 | content/themes/{name}/ | 定义整个站点的外观和前端路由 |
| 插件 | content/plugins/{name}/ | 扩展功能,与主题无关 |
| 小工具 | content/widgets/ 或 core/widgets/ | 可拖拽的前端内容块 |
环境要求
| 项目 | 最低 | 推荐 |
|---|---|---|
| PHP | 7.4 | 8.1+ |
| MySQL | 5.7 | 8.0+ |
| MariaDB | 10.3 | 10.6+ |
必需扩展:pdo、pdo_mysql、json、openssl、zip。
推荐扩展:
intl:slug 音译(中文→拼音)fileinfo:上传文件的 MIME 类型校验curl:更新检查和完整性守卫优先使用,性能优于allow_url_fopenapcu:防火墙的限流可以走内存,性能更好pcntl:队列 Worker 心跳与信号处理
安装与运行
上传源码到网站根目录,浏览器访问域名,自动跳转安装向导:
http://你的域名/install/install.php
安装流程(6 步):
- 启动界面(产品介绍)
- 用户协议
- 环境检测(PHP 版本、扩展、目录权限)
- 数据库配置(主机、端口、库名、用户名、密码、表前缀)
- 站点信息(标题、URL、管理员账号)
- 执行安装(创建表、写配置、生成 admin.php、删除 install/)
安装完成后:
- 自动删除整个
install/目录 - 后台入口默认为
admin.php(强烈建议重命名) - 配置文件写入
config/config.php(数据库)和config/system.php(站点) - 站点 URL 写入
{prefix}options表的site_url键 - 核心文件的 SHA-256 清单写入
storage/manifest.json - 安装元数据(install_id、version)写入
storage/integrity/install.json
本地开发用 PHP 内置服务器即可:
cd /path/to/zerjie
php -S localhost:8080
目录结构
zerjie/
├── index.php 前端入口
├── admin.php 后台入口(可重命名)
├── console.php CLI 入口
├── config/ 配置目录
│ ├── config.php 数据库连接(安装时生成)
│ └── system.php 站点配置(安装时生成)
│
├── core/ 内核(不可修改)
│ ├── zerjie.php 内核引导文件
│ ├── auth.php 用户认证
│ ├── register.php 用户注册
│ ├── database/ 数据库层
│ │ ├── Connection.php PDO 连接包装
│ │ ├── QueryBuilder.php 流式查询构造器
│ │ ├── Migrator.php 迁移管理器
│ │ └── Schema.php 表结构构建器
│ ├── security/ 安全组件
│ │ ├── Csrf.php CSRF 令牌
│ │ ├── Xss.php XSS 过滤器
│ │ ├── Sanitizer.php 输入验证与清理
│ │ ├── UploadGuard.php 上传/压缩包校验
│ │ ├── Firewall.php IP 黑名单 + 限流
│ │ ├── Defense.php 分级防御系统
│ │ └── IntegrityGuard.php 核心文件完整性守卫
│ ├── cache/ 缓存
│ │ ├── CacheManager.php
│ │ ├── FileCache.php
│ │ ├── MemoryCache.php
│ │ └── RedisCache.php
│ ├── log/ 日志
│ │ ├── Logger.php
│ │ ├── FileChannel.php
│ │ └── DatabaseChannel.php
│ ├── queue/ 队列
│ │ ├── Job.php 任务抽象类
│ │ ├── Queue.php 文件队列
│ │ └── Worker.php Worker 进程
│ ├── router/ 路由
│ │ ├── Router.php
│ │ ├── Route.php
│ │ ├── Dispatcher.php
│ │ └── UrlGenerator.php
│ ├── events/ 事件系统
│ │ ├── Dispatcher.php
│ │ ├── Event.php
│ │ └── Listener.php
│ ├── http/ HTTP 抽象
│ │ ├── Kernel.php
│ │ ├── Request.php
│ │ └── Response.php
│ ├── session/ 会话管理
│ │ ├── Session.php
│ │ └── Flash.php
│ ├── i18n/ 多语言
│ │ └── Language.php
│ ├── update/ 更新系统
│ │ ├── UpdateChecker.php
│ │ └── Updater.php
│ ├── console/ CLI
│ │ └── Console.php
│ ├── hook/ 钩子引擎
│ │ └── Hook.php
│ ├── exceptions/ 异常处理
│ │ └── Handler.php
│ ├── widgets/ 系统小工具(可扩展)
│ └── helpers/ 全局辅助函数
│ ├── functions.php 核心辅助函数
│ ├── api.php 后台 / 小工具 / 设置 API
│ ├── mailer.php SMTP 邮件
│ └── debug.php 调试工具
│
├── content/ 内容目录(用户可编辑)
│ ├── themes/ 主题目录
│ ├── plugins/ 插件目录
│ ├── widgets/ 用户小工具目录
│ ├── languages/ 用户语言包(覆盖系统)
│ ├── uploads/ 上传文件
│ └── cache/ 生成的静态资源缓存
│
├── adminbackend/ 后台管理界面
│ ├── AdminController.php 后台控制器
│ ├── admin-menu.php 默认后台菜单
│ ├── assets/ 后台 JS / CSS
│ ├── languages/ 系统语言包
│ └── view/ 后台视图
│
├── templates/ 系统级兜底模板
│ ├── activate.php 账号激活页
│ └── lock-pages/ 站点锁页面模板
│
├── storage/ 运行时存储(禁止公开访问)
│ ├── cache/ 文件缓存
│ ├── logs/ 日志
│ ├── temp/ 临时文件
│ ├── queue/ 队列文件
│ ├── backups/ 备份
│ ├── integrity/ 完整性元数据
│ └── manifest.json 核心文件完整性清单
│
└── install/ 安装向导(安装后自动删除)
请求生命周期
每个 HTTP 请求的执行顺序(以 index.php 为例):
- 入口:
index.php→core/zerjie.php - 常量定义:
ZERJIE_ROOT、ZERJIE_START、ZERJIE_CORE_DIR等 - 卸载守卫:
register_shutdown_function注册 IntegrityGuard 检查钩子(如果已启用) - 配置加载:
config/config.php+config/system.php - 自动加载器:注册
Zerjie\*命名空间的 PSR-4 自动加载 - 维护模式:检测
storage/update/.maintenance标志 - 会话启动:
Zerjie\Session\Session::start()(仅 Web 请求) - 安全响应头:X-Content-Type-Options、X-Frame-Options、CSP 等
- 完整性检查:比对
storage/manifest.json(任一文件被篡改 → 503) - 错误处理:注册全局
error_handler和exception_handler - 加载辅助函数:
helpers/functions.php、api.php、mailer.php - 数据库连接:
zerjie_database_connect() - 预加载配置:从
options表读autoload = 'yes'的键 - 多语言初始化:
Zerjie\I18n\Language::init() - 加载内核模块:
auth.php、register.php - 注册核心服务:Hook、Event Dispatcher、Cache Manager、Logger
- 加载插件:按
active_plugins顺序 require 主文件 - 加载主题:require
content/themes/{active}/functions.php - 加载小工具:glob
content/widgets/*.php - 初始化路由:Router + Dispatcher + UrlGenerator
- 触发
zerjie_ready钩子 ← 所有扩展的入口点 - 执行防御:
Zerjie\Security\Defense::enforce()(非 CLI) - 匹配路由:Dispatcher 匹配 URL → 调用回调
- 输出响应:Response 对象 send
- 卸载:shutdown 钩子触发 IntegrityGuard 检查
zerjie_ready 钩子。zerjie_ready 之前,内核只完成初始化,尚未加载任何业务代码。
钩子与过滤器
Zerjie 提供两套机制:Action(执行代码)和 Filter(修改数据)。这是 Wordpress 风格的经典设计。
基础用法
// 注册 Action(执行代码,无返回值)
add_action('zerjie_ready', function () {
// 内核初始化完成后执行
}, 10);
// 注册 Filter(修改数据,必须有返回值)
add_filter('zerjie_admin_menu', function ($menus) {
$menus[] = ['slug' => 'my-page', 'title' => '我的页面'];
return $menus;
}, 20);
// 触发 Action
do_action('my_custom_action', $arg1, $arg2);
// 应用 Filter
$result = apply_filters('my_custom_filter', $default_value, $arg1);
函数签名
| 函数 | 参数 | 返回值 |
|---|---|---|
add_action($hook, $callback, $priority = 10) |
钩子名、回调、优先级 | 无 |
do_action($hook, ...$args) |
钩子名、任意参数 | 无 |
add_filter($hook, $callback, $priority = 10) |
钩子名、回调、优先级 | 无 |
apply_filters($hook, $value, ...$args) |
钩子名、初始值、额外参数 | 被所有回调处理后的最终值 |
优先级
数字越小越先执行。相同优先级按注册顺序执行。
| 优先级 | 场景 |
|---|---|
| 0-4 | 内核早期处理(如建表、前置检查) |
| 5-9 | 主题/插件基础初始化 |
| 10(默认) | 常规处理 |
| 11-30 | 晚于其他处理(如覆盖菜单、最终修改) |
| 100+ | 最后处理(如输出、记录) |
闭包 vs 命名函数
// 闭包(推荐,作用域清晰,避免命名冲突)
add_action('zerjie_ready', function () {
// ...
});
// 命名函数(便于后续移除)
function my_plugin_init() {
// ...
}
add_action('zerjie_ready', 'my_plugin_init');
return 会导致下游收到 null。
事件系统
事件系统面向对象,适合插件做业务解耦。与钩子系统并存,二者互补。
基础用法
use Zerjie\Events\Event;
// 1. 触发事件
event('user.registered', new Event());
// 2. 监听事件
listen('user.registered', function ($event) {
// 处理逻辑
}, 10); // 第三个参数是优先级
携带数据
用自定义 Event 子类承载数据:
class UserRegisteredEvent extends \Zerjie\Events\Event {
public $userId;
public $username;
public $email;
public function __construct($userId, $username, $email) {
$this->userId = $userId;
$this->username = $username;
$this->email = $email;
}
}
// 触发
event('user.registered', new UserRegisteredEvent(123, 'alice', 'alice@example.com'));
// 监听
listen('user.registered', function ($e) {
echo "新用户:{$e->username}(ID: {$e->userId})";
echo "邮箱:{$e->email}";
});
中止传播
listen('order.created', function ($event) {
if (/* 停止后续监听 */) {
$event->stopPropagation();
}
});
监听器类
复杂业务推荐封装为类(实现 Zerjie\Events\Listener 接口):
namespace MyPlugin;
use Zerjie\Events\Event;
use Zerjie\Events\Listener;
class OrderListener implements Listener {
public function handle(Event $event) {
// 处理订单事件
}
}
// 注册时直接传类名,内核会 new 实例
listen('order.created', 'MyPlugin\OrderListener');
事件 vs 钩子的选择
| 场景 | 推荐 |
|---|---|
| 需要修改返回值 | 过滤器 Filter |
| 只需执行副作用(发通知、写日志) | 动作 Action 或事件 Event |
| 业务解耦、领域事件 | 事件 Event(面向对象) |
| 需要中止传播 | 事件 Event(有 stopPropagation) |
| 快速注册、简单处理 | 钩子 Action/Filter |
主题结构
在 content/themes/ 下新建目录,例如 my-theme/:
content/themes/my-theme/
├── theme.json 主题元信息(必需)
├── functions.php 主题逻辑入口
├── schema.php 私有数据表(可选)
├── templates/ 模板目录
│ ├── partials/ 公共片段
│ │ ├── header.php
│ │ └── footer.php
│ ├── index.php 首页
│ ├── single.php 单篇详情
│ └── 404.php 错误页
├── admin/ 后台视图(可选)
│ └── settings.php
└── assets/ 静态资源
├── css/style.css
└── js/main.js
命名规范:
- 目录名建议只用小写字母、数字、连字符,如
my-theme functions.php是主题的入口,所有钩子注册都在这里templates/存放所有模板文件(文件名对应路由名或页面名)schema.php定义主题私有表,会随主题启用时自动创建
启用主题:
- 后台 → 外观 → 主题 → 找到你的主题 → 点击启用
- 或 CLI:
php console.php theme:activate my-theme
theme.json
主题的身份标识。用于后台主题管理页面,以及判断主题唯一性(同一 id 视为同一主题的不同版本)。
{
"id": "my-theme",
"name": "我的主题",
"version": "1.0.0",
"author": "Your Name",
"description": "一个简洁的博客主题",
"homepage": "https://example.com"
}
字段说明:
| 字段 | 必需 | 说明 |
|---|---|---|
id | 是 | 唯一标识,用于判断同一主题(同名目录不同 id 视为不同主题) |
name | 是 | 显示名称 |
version | 推荐 | 语义化版本号 |
author | 否 | 作者 |
description | 否 | 描述 |
homepage | 否 | 主题主页 |
模板系统
Zerjie 的内核不规定主题的目录结构,主题必须通过 filter 显式注册模板路径。这是为了让主题完全掌控自己的文件布局。
注册模板路径
<?php
if (!defined('ZERJIE_ROOT')) die('Access denied.');
add_filter('zerjie_theme_template_file', function ($file, $template, $theme) {
if ($theme !== 'my-theme') return $file;
// 白名单:只接管本主题提供的模板
$allowed = ['index', 'single', 'page', '404'];
if (!in_array($template, $allowed, true)) return $file;
$path = __DIR__ . '/templates/' . $template . '.php';
return file_exists($path) ? $path : $file;
}, 10, 3);
模板查找顺序
内核的 zerjie_render_theme() 查找顺序(简化为 2 步):
- 触发
zerjie_theme_template_filefilter(主题返回自己的路径) - 兜底:
ZERJIE_ROOT/templates/{tpl}.php(系统级模板)
都找不到 → 输出 Template {name} does not exist.
模板中可用的辅助函数
| 函数 | 说明 |
|---|---|
Helper::e($str) | HTML 转义输出(防 XSS) |
zerjie_get_option($key, $default) | 读取配置 |
zerjie_get_current_user() | 当前登录用户(未登录返回 null) |
zerjie_current_user_can($cap) | 权限检查 |
zerjie_is_logged_in() | 是否已登录 |
__($text) / _e($text) | 多语言(返回 / 输出) |
csrf_field() | 输出 CSRF 隐藏字段 |
csrf_token() | 返回当前 CSRF 令牌 |
the_widgets($area) | 输出指定区域的小工具 |
zerjie_client_ip() | 获取真实客户端 IP |
zerjie_get_favicon_url() | 获取 favicon URL |
flash_get($key) | 读取一次性 flash 消息 |
js_config($extra) | 输出全局 JS 配置属性(含 CSRF、当前用户) |
js_data($data, $key) | 输出自定义 data-* JSON 属性 |
完整示例:首页模板
<?php
// templates/index.php
if (!defined('ZERJIE_ROOT')) die('Access denied.');
$site_title = zerjie_get_option('site_title', 'Zerjie');
$posts = zerjie_db_query('posts', ['status' => 'published'], ['*'], ['id' => 'DESC'], 10);
require __DIR__ . '/partials/header.php';
?>
<main class="container">
<h1><?= Helper::e($site_title) ?></h1>
<?php foreach ($posts as $post): ?>
<article class="post-card">
<h2><?= Helper::e($post['title']) ?></h2>
<p><?= Helper::e(mb_substr(strip_tags($post['content']), 0, 200)) ?>…</p>
</article>
<?php endforeach; ?>
</main>
<?php require __DIR__ . '/partials/footer.php'; ?>
路由注册
在 functions.php 里通过 zerjie_ready 钩子注册路由。
基础用法
add_action('zerjie_ready', function () {
global $zerjie_router;
if (!$zerjie_router) return;
// 首页
$zerjie_router->get('/', function () {
require __DIR__ . '/templates/index.php';
exit;
});
// 关于我们
$zerjie_router->get('/about', function () {
require __DIR__ . '/templates/about.php';
exit;
});
// 文章详情(数字 ID)
$zerjie_router->get('/post/{id:\d+}', function ($id) {
$post = zerjie_db_query('posts', ['id' => (int) $id]);
if (empty($post)) {
http_response_code(404);
require __DIR__ . '/templates/404.php';
exit;
}
$post = $post[0];
require __DIR__ . '/templates/single.php';
exit;
});
// 表单提交
$zerjie_router->post('/contact', function () {
if (!csrf_check()) {
die('CSRF 校验失败');
}
// 处理表单
flash_success('提交成功');
header('Location: /contact');
exit;
});
// 任意 HTTP 方法
$zerjie_router->any('/api/data', function () {
header('Content-Type: application/json');
echo json_encode(['ok' => true]);
exit;
});
});
Route 对象方法
| 方法 | 说明 |
|---|---|
->name($name) | 命名路由,可通过 route() 生成 URL |
->middleware($mw) | 绑定中间件(字符串类名 / 闭包) |
路由分组
$zerjie_router->group('/api/v1', function ($router) {
$router->get('/users', function () { /* GET /api/v1/users */ });
$router->post('/users', function () { /* POST /api/v1/users */ });
$router->get('/posts', function () { /* GET /api/v1/posts */ });
});
生成 URL
// 注册时命名
$zerjie_router->get('/post/{id}', function ($id) { /* ... */ })->name('post');
// 生成 URL
$url = route('post', ['id' => 123]); // → /post/123
参数约束
// 只匹配数字
$zerjie_router->get('/post/{id:\d+}', function ($id) { /* ... */ });
// 只匹配字母数字
$zerjie_router->get('/user/{name:[a-zA-Z0-9_]+}', function ($name) { /* ... */ });
// 默认 [^/]+(匹配除斜杠外任意字符)
$zerjie_router->get('/category/{slug}', function ($slug) { /* ... */ });
// 可选参数(?)
$zerjie_router->get('/page/{num:\d+?}', function ($num = 1) { /* ... */ });
中间件
中间件类需有 handle(Request $req, callable $next) 方法:
namespace MyApp;
use Zerjie\Http\Request;
use Zerjie\Http\Response;
class AuthMiddleware {
public function handle(Request $req, callable $next) {
if (!zerjie_is_logged_in()) {
return new Response('请先登录', 401);
}
return $next($req);
}
}
// 绑定到路由
$zerjie_router->get('/dashboard', function () {
// ...
})->middleware(AuthMiddleware::class);
主题私有表
主题可以携带自己的数据表定义,随主题启用时自动创建,不污染系统安装脚本。所有表名统一加系统前缀,与内核共享同一数据库。
schema.php 格式
返回 SQL 语句数组,用 {prefix} 占位符表示表前缀:
<?php
if (!defined('ZERJIE_ROOT')) die('Access denied.');
return [
"CREATE TABLE IF NOT EXISTS `{prefix}my_posts` (
`id` INT AUTO_INCREMENT PRIMARY KEY,
`title` VARCHAR(191) NOT NULL,
`content` TEXT,
`created_at` DATETIME NOT NULL,
INDEX `created_idx` (`created_at`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;",
"CREATE TABLE IF NOT EXISTS `{prefix}my_comments` (
`id` INT AUTO_INCREMENT PRIMARY KEY,
`post_id` INT NOT NULL,
`content` TEXT,
INDEX `post_idx` (`post_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;",
];
functions.php 中触发建表
add_action('zerjie_ready', function () {
static $done = false;
if ($done) return;
$db = $GLOBALS['zerjie_db'] ?? null;
if (!$db instanceof \PDO) return;
$file = __DIR__ . '/schema.php';
if (!file_exists($file)) { $done = true; return; }
$tables = include $file;
$prefix = zerjie_table_prefix();
foreach ($tables as $sql) {
$sql = str_replace('{prefix}', $prefix, trim((string) $sql));
try { $db->exec($sql); } catch (\PDOException $e) {}
}
$done = true;
}, 5); // 优先级 5,早于业务钩子
注意事项
- 用
CREATE TABLE IF NOT EXISTS,保证幂等 - 开发阶段直接删表重建:本地开发改字段时,手动
DROP TABLE再重新触发即可 - 表名建议加主题前缀:如
forum_topics、shop_orders,避免与其他主题/插件冲突 - 生产环境用迁移系统:正式发布后,字段变更应通过
database/migrations/管理
完整示例:最小博客主题
目录结构
content/themes/my-blog/
├── theme.json
├── functions.php
├── schema.php
├── templates/
│ ├── partials/
│ │ ├── header.php
│ │ └── footer.php
│ ├── index.php
│ └── single.php
└── assets/
└── css/style.css
theme.json
{
"id": "my-blog",
"name": "我的博客",
"version": "1.0.0",
"author": "Your Name",
"description": "极简博客主题"
}
schema.php
<?php
if (!defined('ZERJIE_ROOT')) die('Access denied.');
return [
"CREATE TABLE IF NOT EXISTS `{prefix}blog_posts` (
`id` INT AUTO_INCREMENT PRIMARY KEY,
`title` VARCHAR(255) NOT NULL,
`content` LONGTEXT,
`status` ENUM('draft','published') DEFAULT 'published',
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
INDEX `status_created` (`status`, `created_at`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;",
];
functions.php
<?php
if (!defined('ZERJIE_ROOT')) die('Access denied.');
// 1. 注册模板路径
add_filter('zerjie_theme_template_file', function ($file, $template, $theme) {
if ($theme !== 'my-blog') return $file;
$allowed = ['index', 'single', '404'];
if (!in_array($template, $allowed, true)) return $file;
$path = __DIR__ . '/templates/' . $template . '.php';
return file_exists($path) ? $path : $file;
}, 10, 3);
// 2. 自动建表
add_action('zerjie_ready', function () {
static $done = false;
if ($done) return;
$done = true;
$db = $GLOBALS['zerjie_db'] ?? null;
if (!$db) return;
$file = __DIR__ . '/schema.php';
if (!file_exists($file)) return;
$tables = include $file;
$prefix = zerjie_table_prefix();
foreach ($tables as $sql) {
$sql = str_replace('{prefix}', $prefix, trim($sql));
try { $db->exec($sql); } catch (\PDOException $e) {}
}
}, 5);
// 3. 注册路由
add_action('zerjie_ready', function () {
global $zerjie_router;
if (!$zerjie_router) return;
// 首页
$zerjie_router->get('/', function () {
$db = $GLOBALS['zerjie_db'];
$prefix = zerjie_table_prefix();
$posts = $db->query("SELECT * FROM `{$prefix}blog_posts` WHERE status = 'published' ORDER BY id DESC LIMIT 20")->fetchAll();
require __DIR__ . '/templates/index.php';
exit;
});
// 单篇
$zerjie_router->get('/post/{id:\d+}', function ($id) {
$db = $GLOBALS['zerjie_db'];
$prefix = zerjie_table_prefix();
$stmt = $db->prepare("SELECT * FROM `{$prefix}blog_posts` WHERE id = ? AND status = 'published'");
$stmt->execute([(int) $id]);
$post = $stmt->fetch();
if (!$post) {
http_response_code(404);
require __DIR__ . '/templates/404.php';
exit;
}
require __DIR__ . '/templates/single.php';
exit;
});
});
插件结构
在 content/plugins/ 下新建目录:
content/plugins/my-plugin/
├── plugin.json 插件元信息
├── my-plugin.php 主文件(名称必须与目录名一致)
├── includes/ 辅助 PHP 文件(可选)
└── assets/ 静态资源(可选)
plugin.json:
{
"id": "my-plugin",
"name": "我的插件",
"version": "1.0.0",
"author": "Your Name",
"description": "一个简单的示例插件"
}
主文件 my-plugin.php:
<?php
/**
* Plugin Name: 我的插件
* Description: 一个简单的示例插件
* Version: 1.0.0
* Author: Your Name
*/
if (!defined('ZERJIE_ROOT')) die('Access denied.');
// 插件逻辑
add_action('zerjie_ready', function () {
// 插件初始化
});
加载机制:内核启动时读取 active_plugins option,逐个 require 主文件。
主文件查找顺序:plugin.php → {目录名}.php → index.php。
启用/禁用:
- 后台 → 插件管理 → 找到插件 → 点击启用/禁用
- 或 CLI:
php console.php plugin:activate my-plugin/plugin:deactivate my-plugin
钩子注册
插件最常用的扩展方式就是在 zerjie_ready 里注册各种钩子。
注册后台菜单
add_action('zerjie_ready', function () {
add_filter('zerjie_admin_menu', function ($menus) {
$menus[] = [
'slug' => 'my-plugin-page',
'title' => '我的插件',
'icon' => '🚀',
'view_capability' => 'manage_options',
'manage_capability' => 'manage_options',
'position' => 60,
'callback' => 'my_plugin_render_page',
];
return $menus;
}, 20);
});
function my_plugin_render_page() {
echo '<div style="padding:24px 30px;">';
echo '<h2>我的插件</h2>';
echo '</div>';
}
注入后台 CSS / JS
add_action('zerjie_admin_head', function () {
echo '<link rel="stylesheet" href="/content/plugins/my-plugin/assets/admin.css">';
});
add_action('zerjie_admin_footer', function () {
$ver = filemtime(__DIR__ . '/assets/admin.js');
echo '<script src="/content/plugins/my-plugin/assets/admin.js?v=' . $ver . '"></script>';
});
注册设置字段
add_action('zerjie_ready', function () {
setting_field_register('advanced', 'my_api_key', [
'label' => 'API Key',
'type' => 'text',
'default' => '',
'description' => '第三方服务的 API 密钥',
]);
});
// 读取
$key = get_setting('my_api_key', '');
// 更新
update_setting('my_api_key', 'sk-xxx');
事件监听
插件可以通过事件系统监听内核或其他插件触发的事件:
add_action('zerjie_ready', function () {
// 监听用户注册事件
listen('user.registered', function ($e) {
// 从事件对象取数据
if ($e instanceof \App\Events\UserRegisteredEvent) {
// 发送欢迎邮件
zerjie_send_mail($e->email, '欢迎', '欢迎加入!');
}
});
// 监听订单事件(假设其他插件定义)
listen('order.paid', 'MyPlugin\OrderPaidListener');
});
完整示例:访问统计插件
目录结构
content/plugins/visit-stats/
├── plugin.json
├── visit-stats.php
└── admin-page.php
plugin.json
{
"id": "visit-stats",
"name": "访问统计",
"version": "1.0.0",
"description": "记录每天的访问量"
}
visit-stats.php
<?php
if (!defined('ZERJIE_ROOT')) die('Access denied.');
// 建表
add_action('zerjie_ready', function () {
if (zerjie_get_option('visit_stats_installed') === '1') return;
$db = $GLOBALS['zerjie_db'];
$prefix = zerjie_table_prefix();
$db->exec("CREATE TABLE IF NOT EXISTS `{$prefix}visit_stats` (
`id` INT AUTO_INCREMENT PRIMARY KEY,
`date` DATE NOT NULL,
`count` INT DEFAULT 0,
`updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
UNIQUE KEY `date` (`date`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4");
zerjie_update_option('visit_stats_installed', '1');
}, 5);
// 每次前台请求 +1
add_action('zerjie_ready', function () {
if (defined('ZERJIE_ADMIN_ENTRY') && ZERJIE_ADMIN_ENTRY) return;
$db = $GLOBALS['zerjie_db'];
$prefix = zerjie_table_prefix();
$today = date('Y-m-d');
$db->prepare("INSERT INTO `{$prefix}visit_stats` (date, count)
VALUES (?, 1)
ON DUPLICATE KEY UPDATE count = count + 1")
->execute([$today]);
}, 100);
// 注册后台菜单
add_action('zerjie_ready', function () {
add_filter('zerjie_admin_menu', function ($menus) {
$menus[] = [
'slug' => 'visit-stats',
'title' => '访问统计',
'icon' => '📊',
'view_capability' => 'manage_options',
'manage_capability' => 'manage_options',
'position' => 60,
'callback' => function () {
require __DIR__ . '/admin-page.php';
},
];
return $menus;
}, 20);
});
admin-page.php
<?php
if (!defined('ZERJIE_ROOT')) die('Access denied.');
$db = $GLOBALS['zerjie_db'];
$prefix = zerjie_table_prefix();
$rows = $db->query("SELECT * FROM `{$prefix}visit_stats` ORDER BY date DESC LIMIT 30")->fetchAll();
?>
<div style="padding:24px 30px;">
<h2>📊 访问统计</h2>
<table style="width:100%;border-collapse:collapse;">
<thead>
<tr style="background:#f8fafc;">
<th style="padding:12px;text-align:left;">日期</th>
<th style="padding:12px;text-align:left;">访问量</th>
</tr>
</thead>
<tbody>
<?php foreach ($rows as $r): ?>
<tr style="border-top:1px solid #e9edf2;">
<td style="padding:12px;"><?= Helper::e($r['date']) ?></td>
<td style="padding:12px;"><?= (int) $r['count'] ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
注册小工具
小工具文件放在 content/widgets/ 或 core/widgets/,内核自动加载所有 .php。
简单小工具
<?php
// content/widgets/my-widget.php
if (!defined('ZERJIE_ROOT')) die('Access denied.');
add_action('zerjie_ready', function () {
widget_register(
'my-widget', // 唯一 ID
'我的小工具', // 显示名称
function () { // 渲染回调
?>
<div class="widget-my">
<h3>我的小工具</h3>
<p>这是小工具内容</p>
</div>
<?php
},
[
'sidebar' => 'sidebar', // 默认区域
'position' => 10, // 排序位置
]
);
});
参数说明
| 参数 | 类型 | 说明 |
|---|---|---|
$id | string | 唯一 ID(英文小写、连字符) |
$name | string | 后台显示名称 |
$callback | callable | 渲染回调,输出 HTML |
$params['sidebar'] | string | 默认放置区域 |
$params['position'] | int | 排序位置(小的在前) |
$params['editable'] | bool | 是否可编辑(默认 false) |
$params['render_edit'] | callable | 返回编辑表单 HTML |
$params['save_edit'] | callable | 处理编辑提交 |
前端输出小工具
主题模板中通过 the_widgets() 输出指定区域:
<aside class="sidebar">
<?php the_widgets('sidebar'); ?>
</aside>
<?php if (zerjie_theme_has_widgets('hero')): ?>
<section class="hero">
<?php the_widgets('hero'); ?>
</section>
<?php endif; ?>
内置区域
| 区域 key | 说明 |
|---|---|
hero | 英雄区(首屏大图) |
main | 主内容区 |
sidebar | 侧边栏 |
unplaced | 未放置(预留) |
主题模板中可以调用 the_widgets('自定义区域') 使用任意区域 key。
可编辑小工具
让小工具在后台可以配置参数。需要额外提供 3 个字段:
editable => true:标记可编辑render_edit => callable:返回编辑表单 HTMLsave_edit => callable:处理表单提交
add_action('zerjie_ready', function () {
widget_register(
'my-banner',
'我的横幅',
// 前端渲染
function () {
$inst = $GLOBALS['current_widget_instance'] ?? [];
$instance_id = $inst['instance_id'] ?? 'default';
$data = zerjie_get_option('my_banner_data', []);
$conf = $data[$instance_id] ?? ['title' => '', 'color' => '#000'];
echo '<div style="background:' . Helper::e($conf['color']) . ';padding:20px;">';
echo '<h3>' . Helper::e($conf['title']) . '</h3>';
echo '</div>';
},
[
'sidebar' => 'sidebar',
'position' => 20,
'editable' => true,
// 后台编辑表单
'render_edit' => function ($instance_id) {
$data = zerjie_get_option('my_banner_data', []);
$conf = $data[$instance_id] ?? ['title' => '', 'color' => '#000'];
ob_start();
?>
<div class="form-group">
<label>标题</label>
<input type="text" name="title" value="<?= Helper::e($conf['title']) ?>">
</div>
<div class="form-group">
<label>颜色</label>
<input type="color" name="color" value="<?= Helper::e($conf['color']) ?>">
</div>
<?php
return ob_get_clean();
},
// 保存
'save_edit' => function ($instance_id, $post_data) {
$data = zerjie_get_option('my_banner_data', []);
$data[$instance_id] = [
'title' => trim($post_data['title'] ?? ''),
'color' => trim($post_data['color'] ?? '#000'),
];
zerjie_update_option('my_banner_data', $data);
return ['success' => true, 'message' => '已保存'];
},
]
);
});
完整示例:最新文章小工具
<?php
// content/widgets/latest-posts.php
if (!defined('ZERJIE_ROOT')) die('Access denied.');
add_action('zerjie_ready', function () {
widget_register(
'latest-posts',
'最新文章',
function () {
$inst = $GLOBALS['current_widget_instance'] ?? [];
$instance_id = $inst['instance_id'] ?? 'default';
$data = zerjie_get_option('latest_posts_data', []);
$conf = $data[$instance_id] ?? ['limit' => 5, 'show_date' => true];
$db = $GLOBALS['zerjie_db'];
$prefix = zerjie_table_prefix();
$limit = max(1, min(20, (int) $conf['limit']));
$stmt = $db->query("SELECT id, title, created_at FROM `{$prefix}posts`
WHERE status = 'published'
ORDER BY created_at DESC
LIMIT {$limit}");
$posts = $stmt->fetchAll();
echo '<h3 class="widget-title">最新文章</h3>';
echo '<ul class="widget-post-list">';
foreach ($posts as $p) {
echo '<li>';
echo '<a href="/post/' . (int) $p['id'] . '">' . Helper::e($p['title']) . '</a>';
if (!empty($conf['show_date'])) {
echo ' <time>' . Helper::e($p['created_at']) . '</time>';
}
echo '</li>';
}
echo '</ul>';
},
[
'sidebar' => 'sidebar',
'position' => 5,
'editable' => true,
'render_edit' => function ($instance_id) {
$data = zerjie_get_option('latest_posts_data', []);
$conf = $data[$instance_id] ?? ['limit' => 5, 'show_date' => true];
ob_start();
?>
<div class="form-group">
<label>显示数量</label>
<input type="number" name="limit"
value="<?= (int) $conf['limit'] ?>" min="1" max="20">
</div>
<div class="form-group">
<label>
<input type="checkbox" name="show_date" value="1"
<?= !empty($conf['show_date']) ? 'checked' : '' ?>>
显示日期
</label>
</div>
<?php
return ob_get_clean();
},
'save_edit' => function ($instance_id, $post_data) {
$data = zerjie_get_option('latest_posts_data', []);
$data[$instance_id] = [
'limit' => max(1, min(20, (int) ($post_data['limit'] ?? 5))),
'show_date' => !empty($post_data['show_date']),
];
zerjie_update_option('latest_posts_data', $data);
return ['success' => true, 'message' => '已保存'];
},
]
);
});
注册设置字段
让插件的配置出现在后台「系统设置」页中。
add_action('zerjie_ready', function () {
// 文本字段
setting_field_register('advanced', 'my_api_key', [
'label' => 'API Key',
'type' => 'text',
'default' => '',
'description' => '第三方服务的 API 密钥',
]);
// 复选框
setting_field_register('advanced', 'my_enabled', [
'label' => '启用功能',
'type' => 'checkbox',
'default' => '1',
]);
// 下拉框
setting_field_register('advanced', 'my_mode', [
'label' => '模式',
'type' => 'select',
'default' => 'auto',
'options' => [
'auto' => '自动',
'manual' => '手动',
],
]);
// 文本域
setting_field_register('advanced', 'my_template', [
'label' => '模板',
'type' => 'textarea',
'default' => '默认内容',
]);
// 数字
setting_field_register('advanced', 'my_limit', [
'label' => '数量上限',
'type' => 'number',
'default' => '10',
'min' => 1,
'max' => 100,
]);
}, 10);
读写设置值
// 读取(自动加 'setting_' 前缀)
$apiKey = get_setting('my_api_key', '');
$enabled = get_setting('my_enabled', '0') === '1';
$mode = get_setting('my_mode', 'auto');
// 更新
update_setting('my_api_key', 'sk-xxxxxxxx');
update_setting('my_enabled', '1');
支持的字段类型:text、email、textarea、select、checkbox、number、password。
注入 CSS / JS
后台注入
// 注入到后台 <head>
add_action('zerjie_admin_head', function () {
echo '<link rel="stylesheet" href="/content/plugins/my-plugin/assets/admin.css">';
});
// 注入到后台 </body> 前
add_action('zerjie_admin_footer', function () {
$ver = filemtime(__DIR__ . '/assets/admin.js');
echo '<script src="/content/plugins/my-plugin/assets/admin.js?v=' . $ver . '"></script>';
});
// 仅在小工具管理页注入
add_action('zerjie_admin_footer', function () {
if (($_GET['action'] ?? '') !== 'widget-manager') return;
echo '<script src="/content/plugins/my-plugin/assets/widgets.js"></script>';
});
可用的后台钩子
| 钩子 | 类型 | 说明 |
|---|---|---|
zerjie_admin_head | Action | 后台 <head> 输出 |
zerjie_admin_footer | Action | 后台 </body> 前输出 |
zerjie_admin_topbar | Action | 顶栏输出 |
zerjie_admin_footer_before | Action | 页脚前 |
zerjie_admin_footer_after | Action | 页脚后 |
zerjie_admin_footer_text | Filter | 页脚文字 |
zerjie_admin_page_content_{action} | Filter | 替换某个后台页面内容 |
zerjie_admin_layout_file | Filter | 替换后台整体布局 |
zerjie_admin_login_file | Filter | 替换后台登录页 |
zerjie_admin_header_file | Filter | 自定义后台顶部文件 |
zerjie_admin_footer_file | Filter | 自定义后台底部文件 |
zerjie_dashboard_widgets | Filter | 仪表盘小部件 |
配置读写
所有配置存储在 {prefix}options 表。读取时会缓存到内存,同一请求内多次读取零成本。
读取配置
// 读取(不存在时返回默认值)
$title = zerjie_get_option('site_title', 'Zerjie');
$color = zerjie_get_option('theme_primary_color', '#2563eb');
// 读取数组(自动反序列化)
$settings = zerjie_get_option('my_plugin_settings', []);
if (!empty($settings['enabled'])) {
// ...
}
写入配置
// 写入字符串
zerjie_update_option('site_title', '我的网站');
// 存储数组(自动序列化)
zerjie_update_option('my_plugin_settings', [
'enabled' => true,
'color' => '#2563eb',
'items' => [1, 2, 3],
]);
// 存储数字
zerjie_update_option('my_counter', 42);
autoload 白名单
只有下列 key 会被预加载(安装/首次请求时一次性读入内存),其他 key 首次读取时才查库:
site_title, site_url, active_plugins, nav_menu_primary,
role_caps, admin_allowed_roles, site_lock_enabled,
widget_placements, allow_registration
如需频繁读取自定义配置,可在 core/helpers/functions.php 的 $autoloadKeys 数组中添加 key。
数据库操作
方式一:辅助函数(推荐)
自动加表前缀,避免 SQL 拼接错误。
查询:zerjie_db_query()
// 签名:
// zerjie_db_query($table, $where = [], $select = ['*'], $order = [], $limit = null)
$users = zerjie_db_query(
'users', // 表名(不含前缀)
['role' => 'admin'], // WHERE(等值匹配,AND 连接)
['id', 'username', 'email'], // SELECT
['id' => 'DESC'], // ORDER BY
10 // LIMIT
);
// 只查所有
$all = zerjie_db_query('users');
// 只查指定字段
$names = zerjie_db_query('users', [], ['username']);
// 带排序
$recent = zerjie_db_query('users', [], ['*'], ['registered_at' => 'DESC']);
// 带限制
$top10 = zerjie_db_query('users', [], ['*'], ['login_count' => 'DESC'], 10);
插入:zerjie_db_insert()
// 签名:zerjie_db_insert($table, $data) → 返回自增 ID
$userId = zerjie_db_insert('users', [
'username' => 'alice',
'email' => 'alice@example.com',
'password' => password_hash('secret', PASSWORD_DEFAULT),
'role' => 'user',
'status' => 'active',
]);
更新:zerjie_db_update()
// 签名:zerjie_db_update($table, $data, $where) → 返回影响行数
$affected = zerjie_db_update(
'users',
['status' => 'banned'], // SET
['id' => 5] // WHERE
);
删除:zerjie_db_delete()
// 签名:zerjie_db_delete($table, $where) → 返回影响行数
$affected = zerjie_db_delete('logs', ['id' => 100]);
方式二:原生 PDO(复杂查询)
复杂查询(JOIN、子查询、聚合等)推荐直接用 PDO。
$db = $GLOBALS['zerjie_db'];
$prefix = zerjie_table_prefix();
$stmt = $db->prepare("
SELECT u.username, COUNT(p.id) AS post_count
FROM `{$prefix}users` u
LEFT JOIN `{$prefix}posts` p ON p.author_id = u.id
WHERE u.status = ?
GROUP BY u.id
HAVING post_count > ?
ORDER BY post_count DESC
LIMIT 10
");
$stmt->execute(['active', 5]);
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
方式三:QueryBuilder(流式)
$db = $GLOBALS['zerjie_db'];
$prefix = zerjie_table_prefix();
$qb = new \Zerjie\Database\QueryBuilder($db, 'users');
// 流式查询
$users = $qb->select(['id', 'username', 'email'])
->where('role', '=', 'admin')
->where('status', '=', 'active')
->orderBy('id', 'DESC')
->limit(10)
->get();
// 获取单条
$user = $qb->where('id', '=', 5)->first();
// 分页
$page = $qb->paginate(20, 1);
// → [
// 'items' => [...],
// 'total' => 100,
// 'per_page' => 20,
// 'current_page' => 1,
// 'last_page' => 5,
// ]
// 插入
$qb->insert(['username' => 'bob', 'email' => 'bob@example.com']);
// 更新(必须带 WHERE)
$qb->where('id', '=', 5)->update(['status' => 'banned']);
// 删除(必须带 WHERE)
$qb->where('id', '=', 5)->delete();
// 计数
$count = $qb->count();
// 是否存在
$exists = $qb->where('username', '=', 'alice')->exists();
事务
$db = $GLOBALS['zerjie_db'];
try {
$db->beginTransaction();
$db->prepare("UPDATE `{$prefix}users` SET balance = balance - ? WHERE id = ?")
->execute([100, $fromId]);
$db->prepare("UPDATE `{$prefix}users` SET balance = balance + ? WHERE id = ?")
->execute([100, $toId]);
$db->commit();
} catch (\PDOException $e) {
if ($db->inTransaction()) {
$db->rollBack();
}
throw $e;
}
? 占位符或 bindValue() 传递。
用户与权限
登录状态
// 是否登录
if (zerjie_is_logged_in()) {
// 已登录
}
// 获取当前用户(未登录返回 null)
$user = zerjie_get_current_user();
// [
// 'id' => 1,
// 'username' => 'admin',
// 'email' => 'admin@example.com',
// 'role' => 'super_admin',
// 'status' => 'active',
// 'last_ip' => '127.0.0.1',
// 'last_user_agent' => 'Mozilla/5.0 ...',
// 'last_country' => 'China',
// ...
// ]
登录 / 登出
// 登录:成功返回 true,失败返回 false
$success = zerjie_login($username, $password);
// 失败时读取具体原因
if (!$success) {
$reason = $GLOBALS['zerjie_login_error'];
// 'user_not_found' 账号不存在
// 'invalid_password' 密码错误
// 'account_pending' 账号待激活
// 'account_banned' 账号被封禁
}
// 登出
zerjie_logout();
登录机制
登录成功后,内核会:
- 生成 64 位十六进制令牌(32 字节随机数)
- 用 SHA-256 哈希后存储到
users.session_token字段 - 把原始令牌存入
$_SESSION['session_token'] - 更新
last_login、login_count、last_ip、last_user_agent、last_country - 调用
session_regenerate_id(true)防会话固定 - 轮换 CSRF 令牌
每次请求 zerjie_get_current_user() 会校验:
- Session 中的
user_id是否存在 - Session 中的
session_token的 SHA-256 是否等于数据库中的值
不一致 → 强制下线(比如在另一台设备登录)。
角色
| 角色 | 说明 |
|---|---|
super_admin | 超级管理员,拥有所有权限(强制) |
admin | 管理员,权限由 role_caps 配置 |
user | 普通用户,权限由 role_caps 配置 |
角色常量:ROLE_GUEST、ROLE_USER、ROLE_ADMIN、ROLE_SUPER_ADMIN。
权限检查
if (zerjie_current_user_can('manage_options')) {
// 有权限
}
// 常用权限名:
// read, view_users, manage_users,
// view_settings, manage_settings,
// manage_themes, manage_plugins, manage_widgets,
// manage_taxonomy, manage_nav_menu,
// view_cli, manage_cli, manage_options
登录失败限制
// 检查是否被锁
$check = zerjie_login_attempt_check($identifier);
// → [
// 'locked' => bool, 是否被锁
// 'remaining' => int, 剩余锁定秒数
// 'count' => int, 已失败次数
// 'max' => int, 配置的最大次数
// ]
if ($check['locked']) {
$minutes = ceil($check['remaining'] / 60);
die("账号已锁定,请 {$minutes} 分钟后再试");
}
// 记录一次失败
zerjie_login_attempt_record($identifier);
// 登录成功时重置计数
zerjie_login_attempt_reset($identifier);
相关配置(后台 → 系统 → 登录与注册):
login_max_attempts:最大失败次数(默认 5)login_attempt_window:统计窗口秒数(默认 900)
刷新当前用户缓存
// 修改用户资料后,让缓存失效
zerjie_flush_user_cache();
通常不需要手动调用(内核在登录/登出/修改密码时自动触发)。
用户注册
推荐用统一 API zerjie_register_user(),会自动读取后台的注册开关、默认角色、IP 限制等配置。
最简用法
$result = zerjie_register_user($username, $password, $email);
if ($result['success']) {
$userId = $result['user_id'];
$status = $result['status']; // 'active' | 'pending'
$meta = $result['meta'];
if (!empty($meta['auto_logged_in'])) {
// 注册后已自动登录
} elseif (!empty($meta['activation_email_sent'])) {
// 已发送激活邮件
} elseif (!empty($meta['requires_activation'])) {
// 等待管理员审核
}
} else {
$errors = $result['errors'];
// ['username' => '用户名已存在', 'email' => '邮箱格式不正确']
}
完整参数
$result = zerjie_register_user($username, $password, $email, [
'role' => 'user', // 覆盖默认角色('user' 或 'admin')
'status' => 'active', // 覆盖默认状态('active'/'pending'/'banned')
'display_name' => 'Alice', // 显示名称
'skip_validation' => false, // 是否跳过内置校验
'allow_open' => true, // 是否检查"开放注册"开关
'meta' => ['source' => 'web'], // 附加数据
]);
注册相关配置
后台 → 系统 → 登录与注册 页面可配置:
| 配置 key | 说明 |
|---|---|
allow_registration | 是否开放注册('1'/'0') |
default_role | 默认角色(user / admin) |
register_status | 默认状态(active / pending) |
username_min_length | 用户名最小长度(默认 5) |
password_min_length | 密码最小长度(默认 6) |
password_require_complexity | 密码复杂度要求(数组) |
email_activation | 是否邮件激活('1'/'0') |
auto_login_after_register | 注册后是否自动登录 |
register_ip_limit_count | 同 IP 最大注册数(0 = 不限) |
register_ip_limit_window | 统计窗口(秒) |
激活流程
// 生成激活令牌(24 小时有效)
$token = zerjie_generate_activation_token($userId, 86400);
// 构建激活链接
$link = zerjie_build_activation_link($token);
// → https://your-site.com/activate?token=xxx
// 发送激活邮件
zerjie_send_activation_email($userId, $username, $email);
// 用户点击后,在 /activate 路由里:
$result = zerjie_activate_user_by_token($token);
if ($result['success']) {
// 激活成功
} else {
echo $result['error'];
}
会话与 Flash
会话操作
// 设置
zerjie_session_set('user_pref', 'dark');
// 读取
$pref = zerjie_session_get('user_pref', 'light');
// 判断存在
if (zerjie_session_has('user_pref')) {
// ...
}
// 删除
zerjie_session_delete('user_pref');
// 销毁(清空所有数据 + 重新生成 ID)
zerjie_session_destroy();
底层 Session 类
use Zerjie\Session\Session;
// 启动会话(内核在 Web 请求已自动调用)
Session::start([
'cookie_httponly' => 1,
'cookie_secure' => 0,
'cookie_samesite' => 'Lax',
'use_strict_mode' => 1,
]);
// 其他方法与上面的辅助函数等价
Session::set($key, $value);
Session::get($key, $default);
Session::has($key);
Session::delete($key);
Session::clear();
Session::getId();
Session::regenerateId(true);
Flash 消息(一次性)
适合"操作成功 → 重定向 → 显示提示"的场景。读取后立即自动删除。
// 设置
flash_set('message', '操作成功');
flash_success('保存成功');
flash_error('保存失败');
// 读取(读取后立即删除)
$msg = flash_get('message');
$msg = flash_get('success');
$msg = flash_get('error');
// 判断存在(不删除)
if (Flash::has('message')) {
// ...
}
路由与 URL
注册路由
add_action('zerjie_ready', function () {
global $zerjie_router;
// 静态路径
$zerjie_router->get('/about', function () {
echo 'About Us';
});
// 带数字参数
$zerjie_router->get('/post/{id:\d+}', function ($id) {
echo "Post #{$id}";
});
// 带 slug 参数(默认 [^/]+)
$zerjie_router->get('/category/{slug}', function ($slug) {
echo "Category: {$slug}";
});
// POST
$zerjie_router->post('/contact', function () {
// 处理表单
});
// PUT / DELETE
$zerjie_router->put('/post/{id}', function ($id) { /* ... */ });
$zerjie_router->delete('/post/{id}', function ($id) { /* ... */ });
// 任意 HTTP 方法
$zerjie_router->any('/api/data', function () {
header('Content-Type: application/json');
echo json_encode(['ok' => true]);
});
// 分组
$zerjie_router->group('/api/v1', function ($router) {
$router->get('/users', function () { /* GET /api/v1/users */ });
$router->post('/users', function () { /* POST /api/v1/users */ });
});
});
命名路由 + URL 生成
// 注册时命名
$zerjie_router->get('/post/{id}', function ($id) { /* ... */ })->name('post');
// 生成 URL
$url = route('post', ['id' => 123]); // → /post/123
参数约束
| 写法 | 匹配 |
|---|---|
{id} | 除斜杠外任意字符 |
{id:\d+} | 仅数字 |
{name:[a-zA-Z0-9_]+} | 字母、数字、下划线 |
{slug:[a-z0-9-]+} | 小写字母、数字、连字符 |
Dispatcher 的行为
请求进来后,Dispatcher 会:
- 剥离 query string(
strtok($uri, '?')) - 规范化 URI(去尾部斜杠)
- 按注册顺序匹配路由(
ANY优先匹配任意方法) - 把命名参数通过反射注入到回调
- 自动识别
Request类型参数,注入当前请求对象 - 执行中间件栈(洋葱模型)
- 如果返回
Response对象则自动 send
回调参数自动注入
// 路由参数自动注入
$zerjie_router->get('/post/{id:\d+}/{slug}', function ($id, $slug) {
// $id 和 $slug 是路由匹配到的值
});
// Request 对象自动注入
use Zerjie\Http\Request;
$zerjie_router->post('/submit', function (Request $req) {
$name = $req->input('name');
$ua = $req->header('User-Agent');
$ip = $req->ip();
});
缓存
基础用法
// 写入(TTL 秒)
cache()->set('homepage_data', $data, 3600); // 1 小时
// 读取
$data = cache()->get('homepage_data');
// 判断存在
if (cache()->has('homepage_data')) {
// ...
}
// 删除
cache()->delete('homepage_data');
// 清空全部
cache()->clear();
remember 模式
// 不存在时才执行回调并缓存结果
$expensive = cache()->remember('expensive_key', function () {
return expensive_computation(); // 只在缓存失效时执行
}, 3600);
多驱动
| 驱动 | 说明 | 适用场景 |
|---|---|---|
file | 文件缓存(默认) | 单机、共享主机 |
memory | 内存缓存(请求周期内) | 单次请求内的重复计算 |
redis | Redis 缓存 | 多机集群、高并发 |
// 使用指定的驱动
$redisCache = new \Zerjie\Cache\CacheManager('redis');
$redisCache->set('key', 'value', 3600);
// 或者
use Zerjie\Cache\CacheManager;
$manager = new CacheManager('file');
$manager->driver('redis')->set('key', 'value', 3600);
缓存的存储位置
- FileCache:
storage/cache/{md5(key)}.cache - MemoryCache:静态数组(请求结束即销毁)
- RedisCache:Redis 服务器
日志
基础用法
// 常规记录
logger()->info('用户登录成功', ['user_id' => 5]);
logger()->warning('磁盘空间不足');
logger()->error('数据库连接失败', [
'message' => $e->getMessage(),
'file' => $e->getFile(),
'line' => $e->getLine(),
]);
所有日志级别
| 方法 | 级别 | 说明 |
|---|---|---|
debug($msg, $context) | debug | 调试信息 |
info($msg, $context) | info | 常规信息 |
notice($msg, $context) | notice | 值得注意 |
warning($msg, $context) | warning | 警告 |
error($msg, $context) | error | 错误 |
critical($msg, $context) | critical | 严重错误 |
alert($msg, $context) | alert | 必须立即处理 |
emergency($msg, $context) | emergency | 系统不可用 |
多 channel
| Channel | 输出位置 |
|---|---|
file | storage/logs/YYYY-MM-DD.log |
database | {prefix}logs 表 |
// 指定 channel
logger()->channel('database')->error('落库失败', ['table' => 'orders']);
// 默认 channel 在 core/zerjie.php 中初始化:
$logger = new \Zerjie\Log\Logger('file');
日志文件保留
FileChannel 默认保留最近 30 个按天分割的文件,超出后自动清理最旧的。
邮件
基于 SMTP 发送。后台 → 系统 → SMTP 页面配置发信参数。
基础用法
$result = zerjie_send_mail(
'user@example.com',
'欢迎加入',
'这是邮件的正文内容'
);
if ($result['success']) {
echo '发送成功';
} else {
echo '发送失败:' . $result['message'];
}
HTML 邮件 + 抄送
$result = zerjie_send_mail(
['to1@example.com', 'to2@example.com'], // 收件人(数组)
'每周报告',
'<h1>本周数据</h1><p>访问量:1234</p>',
[
'is_html' => true, // HTML 格式(默认 true)
'cc' => ['cc@example.com'], // 抄送
'bcc' => ['bcc@example.com'], // 密送
'reply_to' => 'reply@example.com', // 回复地址
]
);
SMTP 配置
| 配置 key | 说明 | 默认 |
|---|---|---|
smtp_host | SMTP 服务器地址 | '' |
smtp_port | 端口 | 465 |
smtp_encryption | ssl / tls / none | ssl |
smtp_auth | 是否启用认证 | '1' |
smtp_username | SMTP 用户名 | '' |
smtp_password | SMTP 密码 | '' |
from_email | 发件人邮箱 | '' |
from_name | 发件人名称 | 'Zerjie' |
安全细节
- 邮件头(Subject / From Name)自动过滤 CRLF,防注入
- 非 ASCII 字符自动编码为
=?UTF-8?B?...?= - HTML 邮件自动生成纯文本替代版本(multipart/alternative)
- 所有内容用 base64 编码,避免行长度问题
队列
基于文件系统的轻量队列,适合异步任务(发邮件、生成缩略图、同步数据)。
定义任务
namespace MyPlugin\Jobs;
use Zerjie\Queue\Job;
class SendWelcomeEmail extends Job {
public function handle($payload) {
$userId = (int) ($payload['user_id'] ?? 0);
$email = (string) ($payload['email'] ?? '');
// 执行任务
zerjie_send_mail($email, '欢迎', '欢迎加入!');
// 返回 false 表示失败(会重试)
return true;
}
}
推送任务
use Zerjie\Queue\Queue;
$queue = new Queue('default');
// 立即执行
$queue->push('MyPlugin\Jobs\SendWelcomeEmail', [
'user_id' => 123,
'email' => 'alice@example.com',
]);
// 延迟 60 秒执行
$queue->later(60, 'MyPlugin\Jobs\SendWelcomeEmail', [
'user_id' => 123,
'email' => 'alice@example.com',
]);
处理任务
# 处理 1 个任务后退出
php console.php queue:work
# 处理 100 个任务
php console.php queue:work default 100
# 查看队列长度
php console.php queue:size
Worker 机制
- 认领机制:用
rename原子操作把.job文件从queue/移到processing/,防止多 Worker 抢同一任务 - 心跳:Worker 每 30 秒
touch处理中的文件(PCNTL 支持时) - 重试:任务失败自动重试(默认 3 次,间隔 5 秒)
- 失败归档:超过重试次数后移到
failed/目录 - 陈旧清理:启动时清理超过 30 分钟未完成的 processing 文件
- 信号处理:收到 SIGTERM / SIGINT 时优雅退出(等当前任务完成)
队列目录结构
storage/queue/{queueName}/
├── *.job 待处理任务
├── delayed/ 延迟任务
├── processing/ 处理中任务
└── failed/ 失败归档
自定义 Worker 参数
use Zerjie\Queue\Worker;
$worker = new Worker('default');
$worker->setMaxAttempts(5) // 最大重试次数
->setRetryDelay(10) // 重试间隔秒数
->setSleepSeconds(5) // 空闲时轮询间隔
->setHeartbeatInterval(30) // 心跳间隔
->setStaleTimeout(1800); // 陈旧任务超时
$processed = $worker->work(0); // 0 = 无限循环
多语言
基础用法
// 返回翻译文本(未找到时返回原文)
$text = __('Hello World');
// 带占位符
$text = __('Welcome, {name}!', ['name' => 'Alice']);
// 直接输出
_e('Save Settings');
语言文件位置
语言包按优先级升序加载,后面的覆盖前面的:
adminbackend/languages/{locale}.php(系统语言包,跟随系统更新)content/languages/{locale}.php(用户自定义,优先级更高)
可以通过 filter 追加自定义目录:
add_filter('zerjie_language_paths', function ($paths) {
$paths[] = ZERJIE_ROOT . '/my-lang';
return $paths;
});
语言文件格式
<?php
// content/languages/zh-CN.php
return [
'Hello World' => '你好,世界',
'Welcome, {name}!' => '欢迎,{name}!',
'Save Settings' => '保存设置',
'Password must be at least {n} characters.' => '密码至少需要 {n} 个字符。',
];
语言切换
// 设置当前语言
set_language('zh-CN');
set_language('en-US');
// 获取当前语言
$locale = get_language(); // 'zh-CN'
// 获取所有可用语言
$locales = \Zerjie\I18n\Language::getAvailableLocales();
// ['zh-CN', 'en-US', 'ja-JP', ...]
语言检测优先级
$_SESSION['user_language'](用户在后台切换过)- 浏览器
Accept-Language头(匹配到存在的语言包) - 系统默认语言(
config/system.php的default_language)
语言回退规则
- 精确匹配当前 locale(如
en-US.php) - 父语言(如
en.php) - 默认语言的精确匹配(如
zh-CN.php) - 默认语言的父语言(如
zh.php) - 空数组(翻译函数返回原文)
CSRF 防护
表单中输出隐藏字段
<form method="post">
<?= csrf_field() ?>
<input type="text" name="title">
<button>提交</button>
</form>
会输出:<input type="hidden" name="_csrf" value="...">
服务端校验
// 自动从 POST _csrf 字段或 X-CSRF-Token 头提取
if (!csrf_check()) {
die('CSRF 校验失败');
}
// 手动指定 token
if (!csrf_verify($_POST['_csrf'] ?? '')) {
die('CSRF 校验失败');
}
AJAX 请求头
// 方法 1:使用全局 JS 配置
fetch('/api/endpoint', {
method: 'POST',
headers: {
'X-CSRF-Token': window.ZJ.cfg.csrf,
'Content-Type': 'application/json',
},
body: JSON.stringify({ ... }),
});
// 方法 2:手动取值
const token = document.querySelector('meta[name="csrf-token"]').content;
CSRF 类 API
| 方法 | 说明 |
|---|---|
Csrf::getToken() | 获取当前令牌(无则生成) |
Csrf::generateToken() | 强制生成新令牌 |
Csrf::verifyToken($token) | 校验指定令牌 |
Csrf::clearToken() | 删除当前令牌 |
Csrf::rotate() | 清空 + 生成新令牌 |
Csrf::hiddenField() | 输出隐藏字段 HTML |
Csrf::metaField() | 输出 <meta name="csrf-token"> |
Csrf::extractFromRequest() | 从请求中提取令牌 |
zerjie_login() 成功后会调用 Csrf::rotate(),防止会话固定攻击。
XSS 防护
输出转义(最常用)
<?= Helper::e($userInput) ?>
<?= zerjie_escape_html($html) ?>
// 两者等价,内部都是 htmlspecialchars(ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8')
富文本过滤
// 保留安全的 HTML 标签
$clean = zerjie_xss_filter($html, ['p', 'br', 'strong', 'a']);
zerjie_xss_filter() 使用 DOMDocument 逐节点清理:
- 白名单外的标签直接移除
- 白名单外的属性直接移除
- URL 属性(href / src)检查 scheme(仅允许 http / https / mailto / tel / ftp)
data:仅允许image/gif/image/png/image/jpeg/image/webpsrcset逐个 URL 校验target="_blank"自动追加rel="noopener noreferrer"style里的expression()/javascript:/@import等全部剔除
默认白名单
// 允许的标签
p, br, strong, b, em, i, u, s, del, ins,
h1-h6, ul, ol, li, a, img, figure, figcaption,
blockquote, cite, q, code, pre, kbd, samp,
table, thead, tbody, tfoot, tr, th, td,
span, div, hr, sub, sup
// 允许的属性
href, title, alt, src, srcset,
class, id, target, rel,
width, height, colspan, rowspan
输入验证与清理
通用清理
// 去除首尾空白、删除 HTML 标签
$clean = zerjie_sanitize_input($_POST['comment']);
// 数组会自动递归清理
$data = zerjie_sanitize_input($_POST);
验证函数
| 函数 | 说明 | 返回 |
|---|---|---|
zerjie_validate_email($email) | 验证邮箱 | bool |
zerjie_validate_url($url) | 验证 URL | bool |
zerjie_validate_int($value) | 验证整数 | bool |
Sanitizer::validateFloat($v) | 验证浮点数 | bool |
Sanitizer::validateBool($v) | 验证布尔 | bool / null |
Sanitizer::validatePhone($p) | 验证中国大陆手机号 | bool |
Sanitizer::validateIdCard($id) | 验证中国大陆身份证 | bool |
Sanitizer 类
use Zerjie\Security\Sanitizer;
// 通用清理
$clean = Sanitizer::sanitize($input);
// 各种验证(除 zerjie_* 包裹函数外都直接用)
Sanitizer::validateEmail('a@b.com');
Sanitizer::validateUrl('https://example.com');
Sanitizer::validateInt('123');
Sanitizer::validateFloat('1.23');
Sanitizer::validateBool('true');
Sanitizer::validatePhone('13812345678');
Sanitizer::validateIdCard('110101199001011234');
文件上传安全
校验上传文件
$result = zerjie_validate_upload(
$_FILES['file'],
['jpg', 'png', 'gif'], // 允许扩展名
['image/jpeg', 'image/png', 'image/gif'], // 允许 MIME
5 * 1024 * 1024 // 5MB 上限
);
if (!$result['valid']) {
die($result['error']);
}
UploadGuard::validate() 会依次检查:
- PHP 上传错误码
- 文件大小是否超限
- 扩展名是否在白名单
- ZIP 文件额外做安全检查(防压缩炸弹)
- MIME 类型是否匹配(用 finfo 或 mime_content_type 探测)
- 图片文件额外用
getimagesize()验证
生成安全文件名
$safeName = zerjie_safe_filename($_FILES['file']['name'], 'upload_');
// → upload_66f3a1b2c4d5e.jpg
// 扩展名经过白名单过滤,主名是 uniqid()
完整上传流程
if ($_FILES['file']['error'] === UPLOAD_ERR_OK) {
// 1. 校验
$check = zerjie_validate_upload(
$_FILES['file'],
['jpg', 'png'],
['image/jpeg', 'image/png'],
2097152 // 2MB
);
if (!$check['valid']) {
die($check['error']);
}
// 2. 生成安全文件名
$safeName = zerjie_safe_filename($_FILES['file']['name'], 'user_');
// 3. 移动到目标目录
$target = ZERJIE_CONTENT_DIR . '/uploads/' . $safeName;
if (!is_dir(dirname($target))) {
mkdir(dirname($target), 0755, true);
}
move_uploaded_file($_FILES['file']['tmp_name'], $target);
}
ZIP 安全解压
try {
zerjie_safe_extract_zip('/path/to/file.zip', '/path/to/dest');
} catch (Exception $e) {
// 检测到路径遍历或符号链接
die($e->getMessage());
}
zerjie_safe_extract_zip() 含四道防线:
- 路径规范化:统一分隔符、去首尾斜杠、去
./ - 禁止路径遍历:检测
../片段 - 禁止符号链接:读 ZIP 外部属性,检测 symlink
- 目标目录前缀校验:解压后每个文件必须在目标目录内
防火墙与限流
IP 黑名单
// 检查当前 IP 是否被封禁
if (zerjie_is_ip_blocked()) {
die('您的 IP 已被封禁');
}
// 手动封禁指定 IP
zerjie_block_ip('1.2.3.4');
// 解封
zerjie_unblock_ip('1.2.3.4');
// 黑名单存储位置
// {prefix}options 表的 firewall_blacklist 键(数组)
频率限制
// 签名:zerjie_rate_limit($key = null, $limit = 60, $window = 60)
// $key 为 null 时按 "IP:URI" 组合限流
// 60 秒内最多 60 次
if (!zerjie_rate_limit(null, 60, 60)) {
http_response_code(429);
header('Retry-After: 60');
die('请求过于频繁');
}
// 自定义 key
if (!zerjie_rate_limit('login:' . $ip, 5, 300)) {
// 300 秒内同一 IP 最多登录 5 次
}
驱动优先级
- APCu(如果启用):内存计数,最快
- 文件锁(兜底):
storage/cache/ratelimit/{key}.lock,用flock+ftruncate保证原子性
清理限流文件
use Zerjie\Security\Firewall;
// 清理超过 1 小时未更新的限流文件
$count = Firewall::cleanupRateLimitFiles(3600);
防御响应系统
完整的分级防御系统,由 Zerjie\Security\Defense 提供。包含:
- 全站请求量分级告警与响应(L1~L5)
- 单 IP 高频访问自动封禁
- 敏感路径扫描检测(.env / .git / wp-admin 等)
- 邮件通知
自动启用
内核在 zerjie_ready 之后自动调用 Defense::enforce(),无需手动启用。
配置
后台 → 防御响应 → 防御设置 页面可视化配置。配置存储于 {prefix}options 表的 defense_config 键。
核心方法
| 方法 | 说明 |
|---|---|
Defense::enforce() | 执行防御检查(内核自动调用) |
Defense::getStatus() | 返回当前防御状态 |
Defense::getConfig() | 返回当前配置 |
Defense::saveConfig($config) | 保存配置 |
Defense::banIp($ip, $forcePermanent) | 封禁 IP |
Defense::unbanIp($ip, $resetCount) | 解封 IP |
Defense::listBannedIps() | 列出所有生效的封禁 |
Defense::isBanned($ip) | 检查 IP 是否被封 |
Defense::clear() | 清除全站防御等级 |
Defense::setLevel($level, $reason, $duration) | 手动设置防御等级 |
Defense::cleanupSlots($keepMinutes) | 清理旧的计数器行 |
Defense::cleanupExpiredBans($days) | 清理过期的临时封禁 |
分级说明
| 等级 | 默认阈值 | 作用范围 | 响应 |
|---|---|---|---|
| L1 | 60s / 5000 次 | 全站 | 仅记录 |
| L2 | 60s / 10000 次 | 全站 | 仅记录 |
| L3 | 60s / 50000 次 | 写操作 | 503,持续 60s |
| L4 | 300s / 100000 次 | 敏感路径 | 503,持续 300s |
| L5 | 300s / 500000 次 | 全站 | 503,需手动解除 |
阈值与范围可调
通过后台 → 防御响应 → 防御设置 可调整:
- 每级阈值(窗口内的请求数)
- 窗口秒数
- 持续时间(0 = 永久,需手动解除)
- 作用范围:
all(全部请求)/write(仅写操作)/sensitive(仅敏感路径)
单 IP 自动黑名单
当单个 IP 在窗口内超过阈值,自动临时封禁。累计封禁次数达到阈值后永久封禁。
敏感路径扫描
内置一批正则匹配常见攻击路径:
/.env 配置文件泄露
/.git 版本控制
/wp-admin 后台探测
/phpmyadmin PMA 探测
*.sql 备份文件
/composer.json 依赖清单
/Dockerfile 容器配置
CLAUDE.md AI 工具配置(2024+ 新型探测)
...
5 分钟内同一 IP 命中 N 次(默认 5 次)自动封禁。可通过 filter 调整:
add_filter('zerjie_defense_suspicious_patterns', function ($patterns) {
$patterns[] = '#/my-secret-path#i';
return $patterns;
});
add_filter('zerjie_defense_scan_threshold', function () {
return 3; // 5 分钟内 3 次就封
});
邮件通知
可配置防御事件发生时给指定用户发邮件。后台 → 防御响应 → 防御设置 → 邮件通知。
订阅事件:
ban_temp临时封禁ban_perm永久封禁unban解封level_alert等级告警(L3+ 触发)manual_clear手动清除等级manual_freeze手动进入 L5
节流:同一事件类型在 throttle 秒内只发一封,防止攻击期间邮件轰炸。
数据表
{prefix}defense_slots:全站每分钟请求数{prefix}defense_ip_slots:每 IP 每分钟请求数{prefix}defense_ip_bans:IP 封禁记录{prefix}defense_scan_log:敏感路径扫描日志
完整性守卫
Zerjie\Security\IntegrityGuard 周期性比对核心文件与官方清单,发现被篡改的文件会自动从官方服务器下载原始版本覆盖。
启用 / 关闭
# 启用(会二次确认,警告"会覆盖你修改过的核心文件")
php console.php integrity:on
# 查看状态
php console.php integrity:status
# 手动触发检查
php console.php integrity:check
# 关闭
php console.php integrity:off
工作方式
- 启用时生成
storage/integrity/.enabled锁文件 - 每次请求结束时(shutdown hook)检查距上次检查是否超过 1 小时
- 超过则调用
checkAndRestore(): - 从官方服务器拉取清单
- 逐个文件比对 SHA-256
- 不一致的文件从官方下载覆盖
- 检查时间戳写入
storage/integrity/.last_check
官方清单的排除项
清单不会包含以下路径(防止覆盖站点私有数据):
install/ 安装后会被删除
config/ 每站的数据库配置不同
storage/ 运行时数据
content/ 主题、插件、小工具、上传文件
templates/lock-pages/ 用户可上传自定义锁页模板
admin.php 站点独有,安装时生成,用户可重命名
IntegrityGuard 类 API
| 方法 | 说明 |
|---|---|
IntegrityGuard::enable() | 启用 |
IntegrityGuard::disable() | 关闭 |
IntegrityGuard::isEnabled() | 是否已启用 |
IntegrityGuard::status() | 返回状态信息 |
IntegrityGuard::checkAndRestore() | 执行检查 |
IntegrityGuard::shouldCheck() | 是否需要检查(间隔判断) |
本地完整性检查(独立于 IntegrityGuard)
安装时生成的 storage/manifest.json 会在每次请求开始时用于比对核心文件。任一文件被修改或缺失 → 立即返回 503。
通过 CLI 重建清单:
# 重新生成清单(修改核心文件后手动调用)
php console.php security:manifest
# 检查核心文件是否完整
php console.php security:check
检查更新
Zerjie\Update\UpdateChecker 从官方服务器检查是否有新版本。
CLI
php console.php update:check
PHP API
use Zerjie\Update\UpdateChecker;
// 检查(带缓存,默认 1 小时)
$info = UpdateChecker::check();
// 强制检查(忽略缓存)
$info = UpdateChecker::check(true);
// 返回值:
// [
// 'status' => 'update_available' | 'up_to_date' | 'network_error'
// | 'invalid_response' | 'no_versions' | 'unknown_error',
// 'current' => '1.0.0',
// 'latest' => '1.2.0', // status = update_available
// 'released_at' => '2026-01-15T...',
// 'file_count' => 128,
// 'size' => 1048576, // 字节
// 'sha256' => 'abc...',
// 'min_php' => '7.4.0',
// 'changelog' => '新增 xxx\n修复 yyy',
// 'download_url' => 'https://...',
// ]
// 快捷判断
if (UpdateChecker::hasUpdate()) {
// 有新版本
}
// 清理缓存
UpdateChecker::clearCache();
缓存
- 缓存文件:
storage/update/.latest.json - 缓存时间:
UpdateChecker::CACHE_TTL(默认 3600 秒)
安全措施
- URL 必须在可信 base 下(防止 SSRF)
- HTTPS 强制
- 不跟随重定向
- SSL 证书验证
- 只允许 CURLPROTO_HTTPS
下载与安装
Zerjie\Update\Updater 负责下载更新包、解压、校验、备份、安装。
CLI
# 下载最新更新包(不安装)
php console.php update:download
# 下载 + 安装(一步到位)
php console.php update:apply
PHP API(分步执行)
use Zerjie\Update\UpdateChecker;
use Zerjie\Update\Updater;
// 1. 检查
$info = UpdateChecker::check(true);
if ($info['status'] !== 'update_available') {
die('无可用更新');
}
// 2. 下载(保存到 storage/update/packages/)
$dl = Updater::download($info);
if (!$dl['success']) {
die($dl['error']);
}
// → ['success' => true, 'file' => '/path/to/zerjie-1.2.0.zip', 'size' => ...]
// 3. 解压 + 校验
$prep = Updater::prepare($dl['file']);
if (!$prep['success']) {
die($prep['error']);
}
// → ['success' => true, 'dir' => '/path/to/extracted',
// 'files' => [...], 'version' => '1.2.0']
// 4. 应用
$apply = Updater::apply($prep['dir'], $prep['files'], '1.2.0');
if (!$apply['success']) {
die($apply['error']);
}
// → ['success' => true, 'backup' => '/path/to/backup', 'applied' => 128]
安装过程
- 加锁:
storage/update/.lock(防止并发安装) - 进入维护模式:写
storage/update/.maintenance,访客看到 503 - 备份:把被覆盖的文件复制到
storage/backups/updates/{timestamp}/ - 备份旧 meta:
.old-meta.json - 复制文件:从解压目录逐个复制到站点(仅允许的路径)
- 更新 install.json:写入新版本号
- 清理更新缓存:删除
.latest.json - 重建清单:重新生成
storage/manifest.json - 运行迁移:
Migrator::run()(如果有数据库变更) - 退出维护模式:删除
.maintenance - 释放锁
允许更新的路径
更新包只能修改以下路径,其余一概忽略:
core/ 核心
adminbackend/ 后台
assets/ 系统级静态资源
templates/ 兜底模板
index.php
console.php
favicon.ico
favicon.png
更新包不能修改:
admin.php 站点独有
templates/lock-pages/ 用户自定义
config/ 站点配置
storage/ 运行时数据
content/ 主题、插件、小工具
失败自动回滚
安装过程中任何一步失败,会自动从备份目录恢复所有被覆盖的文件。
回滚
列出所有备份
php console.php update:backups
回滚到指定备份
# 备份名形如 20260115_143022
php console.php update:rollback 20260115_143022
PHP API
use Zerjie\Update\Updater;
// 列出备份
$list = Updater::listBackups();
// → [
// ['path' => '...', 'name' => '20260115_143022', 'time' => '2026-01-15 14:30:22', 'meta' => [...]],
// ...
// ]
// 回滚
$result = Updater::rollback('20260115_143022');
// → ['success' => true, 'restored' => 128]
回滚过程
- 遍历备份目录,把每个文件复制回原位
- 恢复
install.json(版本号回滚) - 清理更新缓存
- 重建清单
storage/backups/updates/,或写定时任务。
CLI 命令
通过 php console.php <command> 在服务器终端执行。
查看所有命令
php console.php # 或
php console.php help
php console.php list
命令速查
| 命令 | 说明 |
|---|---|
| 用户管理 | |
user:list | 列出所有用户 |
user:add <用户名> <密码> <邮箱> [角色] | 添加用户 |
user:delete <ID> | 删除用户 |
user:password <ID或用户名> <新密码> [--force] | 改密码 |
user:promote <ID或用户名> <角色> [--force] | 改角色 |
user:count | 统计各角色用户数 |
| 主题 | |
theme:list | 列出已安装主题 |
theme:activate <name> | 启用主题 |
theme:info <name> | 主题详情 |
| 插件 | |
plugin:list | 列出插件 |
plugin:activate <name> | 启用插件 |
plugin:deactivate <name> | 禁用插件 |
plugin:info <name> | 插件详情 |
| 缓存 / 日志 | |
cache:clear | 清空文件缓存 |
cache:stats | 缓存统计 |
log:clear | 清空日志 |
log:tail [n] | 查看最新 n 行日志 |
log:list | 列出所有日志文件 |
| 数据库 | |
db:list-tables | 列出所有表 |
db:table-info <name> | 表结构 |
db:size | 数据库/表大小 |
db:optimize | 优化系统表 |
db:backup | 备份数据库 |
| 迁移 | |
migrate | 执行所有待执行的迁移 |
migrate:rollback [steps] | 回滚最近 n 次迁移 |
migrate:reset | 回滚所有迁移 |
| 系统 | |
system:info | 系统信息 |
system:health | 系统健康检查 |
system:debug | 运行环境调试信息 |
system:routes | 列出所有路由 |
system:env | 服务器环境变量 |
system:time | 当前时间 |
system:clear-temp | 清理临时文件 |
| 调试模式 | |
debug:on / debug:off | 开关调试模式 |
debug:status | 调试模式状态 |
debug:toggle | 切换调试模式 |
| 队列 | |
queue:work [name] [max] | 启动队列 worker |
queue:size [name] | 队列长度 |
| 安全 | |
security:manifest | 生成完整性清单 |
security:check | 检查核心文件完整性 |
defense:status | 防御状态 |
defense:clear | 清除防御等级 |
defense:cleanup | 清理旧计数器 |
integrity:on / integrity:off | 开关完整性守卫 |
integrity:status | 完整性守卫状态 |
integrity:check | 手动触发完整性检查 |
| 更新 | |
update:check | 检查更新 |
update:download | 下载更新包 |
update:apply | 下载并应用更新 |
update:backups | 列出更新备份 |
update:rollback <name> | 回滚更新 |
示例
# 添加管理员
php console.php user:add alice secret123 alice@example.com admin
# 清空缓存并查看日志
php console.php cache:clear
php console.php log:tail 50
# 备份数据库
php console.php db:backup
# 检查系统健康
php console.php system:health
# 启用主题
php console.php theme:activate my-theme
# 执行待处理的迁移
php console.php migrate
数据表结构
系统表都以 {prefix} 为前缀(默认 wp_),安装时自定义。
| 表名 | 用途 |
|---|---|
{prefix}users | 用户账户 |
{prefix}options | 系统配置(键值对) |
{prefix}logs | 数据库日志(DatabaseChannel) |
{prefix}migrations | 迁移记录 |
{prefix}taxonomies | 分类 |
{prefix}term_relationships | 内容-分类关联 |
{prefix}menus | 导航菜单 |
{prefix}menu_items | 菜单项 |
{prefix}invite_codes | 邀请码(站点锁) |
{prefix}register_log | IP 注册日志 |
{prefix}login_attempts | 登录失败记录 |
{prefix}defense_slots | 全站每分钟请求数 |
{prefix}defense_ip_slots | 每 IP 每分钟请求数 |
{prefix}defense_ip_bans | IP 封禁记录 |
{prefix}defense_scan_log | 敏感路径扫描日志 |
users 表字段
| 字段 | 类型 | 说明 |
|---|---|---|
| id | INT | 主键 |
| username | VARCHAR(60) | 用户名(唯一) |
| VARCHAR(100) | 邮箱(唯一,可空) | |
| password | VARCHAR(255) | 密码哈希(bcrypt) |
| display_name | VARCHAR(100) | 显示名 |
| role | ENUM | super_admin / admin / user |
| status | ENUM | active / pending / banned |
| registered_at | TIMESTAMP | 注册时间 |
| last_login | TIMESTAMP | 最后登录 |
| login_count | INT | 登录次数 |
| session_token | VARCHAR(255) | 会话令牌(SHA-256 哈希) |
| last_ip | VARCHAR(45) | 最后登录 IP |
| last_user_agent | VARCHAR(500) | 最后登录 UA |
| last_country | VARCHAR(100) | 最后登录国家 |
options 表字段
| 字段 | 类型 | 说明 |
|---|---|---|
| option_key | VARCHAR(191) | 主键 |
| option_value | LONGTEXT | 值(数组/对象序列化存储) |
| autoload | ENUM('yes','no') | 是否预加载 |
| updated_at | TIMESTAMP | 更新时间 |
新增自定义表
插件/主题可以自己建表,建议:
- 表名前缀使用
{prefix},与系统保持一致 - 首次安装时用
zerjie_get_option()做标记,避免重复建表 - 用
CREATE TABLE IF NOT EXISTS保证幂等
add_action('zerjie_ready', function () {
if (zerjie_get_option('my_plugin_installed') === '1') return;
$db = $GLOBALS['zerjie_db'];
$prefix = zerjie_table_prefix();
$db->exec("CREATE TABLE IF NOT EXISTS `{$prefix}my_data` (
`id` INT AUTO_INCREMENT PRIMARY KEY,
`name` VARCHAR(100),
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4");
zerjie_update_option('my_plugin_installed', '1');
}, 5);
钩子速查表
内核钩子
| 钩子 | 类型 | 说明 |
|---|---|---|
zerjie_ready | Action | 内核初始化完成(主题、插件主入口) |
zerjie_admin_menu | Filter | 后台菜单注册 |
zerjie_admin_head | Action | 后台 <head> 输出 |
zerjie_admin_footer | Action | 后台 </body> 前输出 |
zerjie_admin_topbar | Action | 顶栏输出 |
zerjie_admin_footer_before | Action | 页脚前 |
zerjie_admin_footer_after | Action | 页脚后 |
zerjie_admin_footer_text | Filter | 页脚文字 |
zerjie_admin_page_content_{action} | Filter | 替换某个后台页面内容 |
zerjie_admin_layout_file | Filter | 替换后台整体布局 |
zerjie_admin_login_file | Filter | 替换后台登录页 |
zerjie_admin_header_file | Filter | 自定义后台顶部文件 |
zerjie_admin_footer_file | Filter | 自定义后台底部文件 |
zerjie_theme_template_file | Filter | 主题模板路径注册 |
zerjie_widgets | Filter | 小工具注册 |
zerjie_dashboard_widgets | Filter | 仪表盘小部件 |
zerjie_settings_fields | Filter | 设置字段注册 |
zerjie_language_paths | Filter | 多语言包路径追加 |
zerjie_theme_head | Action | 前端 <head> 输出(主题触发) |
zerjie_theme_footer | Action | 前端 </body> 前(主题触发) |
注册相关
| 钩子 | 类型 | 说明 |
|---|---|---|
zerjie_registration_open | Filter | 是否开放注册 |
zerjie_default_registration_role | Filter | 默认角色 |
zerjie_default_registration_status | Filter | 默认状态 |
zerjie_username_available | Filter | 用户名可用性 |
zerjie_email_available | Filter | 邮箱可用性 |
zerjie_username_min_length | Filter | 用户名最小长度 |
zerjie_password_min_length | Filter | 密码最小长度 |
zerjie_password_strength | Filter | 密码强度校验 |
zerjie_register_args | Filter | 注册参数 |
zerjie_register_validation | Filter | 注册数据校验 |
zerjie_register_user_data | Filter | 最终插入数据 |
zerjie_register_result | Filter | 注册结果 |
zerjie_before_register | Action | 注册前 |
zerjie_after_register | Action | 注册成功后 |
zerjie_user_activated | Action | 用户激活后 |
zerjie_user_pending | Action | 待审核用户 |
zerjie_registration_failed | Action | 注册失败 |
防御系统
| 钩子 | 类型 | 说明 |
|---|---|---|
zerjie_defense_suspicious_patterns | Filter | 敏感路径正则列表 |
zerjie_defense_scan_threshold | Filter | 敏感路径扫描触发封禁的阈值 |
zerjie_defense_suspicious_request | Action | 命中敏感路径时广播(插件可订阅) |
常见问题
Q1:修改主题文件后不生效?
可能是浏览器缓存了旧 CSS / JS。在引用资源时加版本号强制刷新:
<link rel="stylesheet" href="/content/themes/my-theme/assets/css/style.css?v=<?= filemtime(__DIR__ . '/assets/css/style.css') ?>">
Q2:后台入口忘了叫什么?
查看 config/system.php 中的 admin_entry 字段;或执行:
php console.php system:info
Q3:用户登录状态频繁失效?
可能是 session_token 校验失败。原因:
- 其他设备登录了同一账号(会强制下线)
- 密码被修改(会重置 session_token)
- 会话超时(默认 24 小时)
Q4:如何让插件覆盖后台菜单的默认项?
add_filter('zerjie_admin_menu', function ($menus) {
foreach ($menus as &$m) {
if (($m['slug'] ?? '') === 'dashboard') {
$m['title'] = '我的仪表盘';
}
}
return $menus;
}, 20); // 优先级 20,晚于默认的 10
Q5:如何查看日志?
# 查看最新 50 行
php console.php log:tail 50
# 或直接读文件
tail -f storage/logs/$(date +%Y-%m-%d).log
Q6:debug 模式下调试代码
php console.php debug:on
// 代码中
dump($variable); // 打印变量
dd($variable); // 打印后终止
Q7:升级数据库表结构
用迁移系统:
// 在 database/migrations/ 下新建 20260101_add_column.php
<?php
return [
'up' => function ($db, $prefix) {
$db->exec("ALTER TABLE `{$prefix}users` ADD COLUMN `nickname` VARCHAR(60)");
},
'down' => function ($db, $prefix) {
$db->exec("ALTER TABLE `{$prefix}users` DROP COLUMN `nickname`");
},
];
// 执行
php console.php migrate
Q8:CSP 拦截了内联脚本?
Zerjie 的 CSP 策略是 script-src 'self',禁止内联脚本。解决方案:
- 把脚本写到独立
.js文件中,通过<script src="..."></script>引用 - 不要用
<script>...代码...</script>内联 - 不要用
onclick="..."等内联事件,改用事件委托
Q9:主题首页显示"Template index does not exist"?
主题没有注册 zerjie_theme_template_file filter。解决方案:
- 在
functions.php里注册 filter,返回主题的模板路径 - 或者直接注册
/路由,明确处理首页
// 方式 1:filter
add_filter('zerjie_theme_template_file', function ($file, $template, $theme) {
if ($theme !== 'my-theme') return $file;
$path = __DIR__ . '/templates/' . $template . '.php';
return file_exists($path) ? $path : $file;
}, 10, 3);
// 方式 2:路由(推荐,更明确)
add_action('zerjie_ready', function () {
global $zerjie_router;
$zerjie_router->get('/', function () {
require __DIR__ . '/templates/index.php';
exit;
});
});
Q10:小工具拖拽到区域后前台不显示?
检查三点:
- 主题模板是否调用了
the_widgets('区域名') - 区域名是否与后台拖拽时的一致
- 小工具的
sidebar参数默认值是否匹配(只是默认,最终由拖拽决定)
Q11:开启完整性守卫后站点 503?
可能是清单中某个核心文件与你本地的 SHA-256 不一致。检查步骤:
- 查看
storage/logs/security.log找出被标记的文件 - 如果你故意修改过核心文件,应该关闭完整性守卫:
php console.php integrity:off - 如果是意外篡改,用
php console.php security:check定位 - 需要临时放行,重新生成清单:
php console.php security:manifest
Q12:如何修改防御系统的敏感路径规则?
add_filter('zerjie_defense_suspicious_patterns', function ($patterns) {
// 追加规则
$patterns[] = '#/my-secret-url#i';
// 移除某条规则(比如想放行 .env)
$patterns = array_filter($patterns, function ($p) {
return strpos($p, '.env') === false;
});
return array_values($patterns);
});
Q13:邮件发送失败怎么排查?
- 后台 → 系统 → SMTP 页面,检查配置是否完整(host / username / password / from_email)
- 用「发送测试」按钮验证 SMTP 连接
- 查看
storage/logs/YYYY-MM-DD.log,搜索[DEFENSE-EMAIL]或mail - 常见问题:端口被云服务商封锁(改用 587 + TLS)、密码用的是授权码而非登录密码(Gmail / QQ 邮箱)
Q14:如何切换缓存驱动到 Redis?
在 core/zerjie.php 里修改初始化代码:
// 原代码
$cacheManager = new Zerjie\Cache\CacheManager('file');
// 改为
$cacheManager = new Zerjie\Cache\CacheManager('redis');
并在 RedisCache 构造时传入配置(host / port / password / database / prefix)。