架构总览
Clara 无框架自研架构:核心类、控制器与路由、目录组织与运行机制,理解系统全貌的起点。
01 · 架构总览
1. 技术栈与设计哲学
| 维度 | 选型 | 说明 |
|---|---|---|
| 语言 | PHP 7.4 – 8.5 | 兼容宽版本区间,虚拟主机友好 |
| 数据库 | MySQL 5.7+ / 8.0(PDO) | utf8mb4,表名统一前缀 |
| 依赖 | 零 Composer 依赖 | SMTP、云存储签名(七牛/OSS/COS)均为原生实现 |
| 前端 | 原生 JS + CSS(无构建步骤) | `assets/js/app.js`、`assets/css/app.css` 直接引用 |
| 架构 | 单入口 MVC | 前台 `index.php`、后台 `admin.php` 双入口 |
| 扩展 | 钩子(Hook)+ 插件(Plugin) | 参照 WordPress / Typecho 插件机制 |
设计哲学可概括为三点:
- 静态门面风格:核心能力(`Db`、`Auth`、`View`、`Settings`、`Plugin`、`Hook`…)全部以静态类提供,控制器与视图内直接调用,无容器、无依赖注入。
- 约定优于配置:后台路由按 `Admin{Module}Controller` 命名自动映射;插件按目录名 + `plugin.php` 约定自动发现。
- 插件代码与核心解耦:所有扩展点收敛到 `Hook::fire()/filter()`,插件抛错只记日志不阻断主流程。
2. 目录结构
luntan/
├── index.php # 前台入口(未安装时跳转 install/)
├── admin.php # 后台入口
├── restore.php # 数据库还原 CLI(仅命令行运行,配套后台备份下载)
├── config/
│ └── config.php # 安装程序生成的配置(db / prefix / timezone / key)
├── app/
│ ├── bootstrap.php # 引导 + App 调度器(自动加载/会话/错误处理)
│ ├── helpers.php # 全局辅助函数聚合入口(require 13 个域文件,v2026.09.15 拆分)
│ ├── helpers/ # 辅助函数域文件(13 个:general/icon/user/economy/render/attachment/mention/notify/forum/perm/captcha/admin-sec/runtime;173 函数 + 3 个顶级 Cron 注册,函数体与拆分前零差异)
│ ├── routes.php # 前台路由表
│ ├── admin_routes.php # 后台路由表(含 AdminRouter 约定映射)
│ ├── Core/ # 核心框架类(15 个)
│ │ ├── Router.php # 路由器({param} 占位符 / .html 伪静态)
│ │ ├── Db.php # PDO 封装(q/one/all/val/insert/update…)
│ │ ├── Auth.php # 认证与权限(会话/记住我/组权限/版块权限/会员)
│ │ ├── Plugin.php # 插件管理(扫描/启停/配置/元信息解析)
│ │ ├── Hook.php # 钩子系统(action: fire / filter: filter)
│ │ ├── View.php # 视图渲染(render/display/renderFile)
│ │ ├── Settings.php # 站点设置 KV 缓存(文件缓存 60s + 写穿)
│ │ ├── SessionStore.php # 自定义文件会话存储(读不锁/写原子,并发优化)
│ │ ├── Cache.php # KV 缓存抽象层(Redis/文件双驱动,v2026.09.06)
│ │ ├── Cron.php # 系统计划任务(懒触发 + 外部触发,v2026.09.06)
│ │ ├── Request.php # 请求封装(GET/POST/IP/UA/wantsJson)
│ │ ├── Csrf.php # CSRF 令牌(表单 + X-CSRF-Token 头)
│ │ ├── Validator.php # 输入校验器(required/email/unique…)
│ │ ├── Controller.php # 前台控制器基类(POST 自动 CSRF)
│ │ └── AdminController.php # 后台控制器基类(套 admin/layout)
│ ├── Controllers/ # 前台业务控制器(23 个)
│ ├── Admin/ # 后台控制器(26 个,Admin{Module}Controller 命名)
│ ├── Services/ # 服务层
│ │ ├── GeoService.php # GEO/AI 搜索优化(answers/llms/JSON-LD/推送/体检)
│ │ ├── CreditRuleService.php # 积分规则引擎(事件钩子 → 货币发放)
│ │ ├── Mailer.php # 原生 SMTP 客户端
│ │ ├── MedalService.php # 勋章自动检测与授予
│ │ ├── SeoService.php # sitemap / RSS 生成
│ │ └── Storage/ # 云存储工厂 + 4 驱动(local/qiniu/oss/cos)
│ └── Views/ # 视图(PHP 模板)
│ ├── layout.php # 前台主布局(导航/页脚/钩子点位)
│ ├── *.php # 前台页面(home/thread/forum/uc/answers/pm/…)
│ ├── partials/ # 复用组件(editor/thread-row/left-quick-nav/cat-nav…)
│ └── admin/ # 后台视图(layout.php + 各管理页)
├── content/plugins/ # 插件目录(14 个,每插件一个子目录)
│ ├── ai_reply/ # AI 智能回复(v1.4.1)
│ ├── ai_writer/ # AI 智能写作(v1.5.5)
│ ├── api_hub/ # API 工具箱(v1.0.2)
│ ├── site_rank/ # 社区排行榜(v1.0.0)
│ ├── member_wall/ # 首页会员墙(v1.0.0)
│ ├── ad_self/ # 广告位自助投放(v1.7.0)
│ ├── lottery/ # 幸运抽奖(v1.3.0)
│ ├── ks_exam/ # 入站考试(v1.0.0)
│ ├── geo_qa_factory/ # GEO 问答工厂(v1.0.0)
│ ├── brand_landing/ # 品牌落地页(v1.0.0)
│ ├── quick_reply/ # 快速回帖(v1.1.0)
│ ├── thread_visitors/ # 帖子访客展示(v1.0.1)
│ ├── copy_tail/ # 复制保护小尾巴(v1.0.0)
│ └── mourning/ # 黑白哀悼模式(v1.3.0)
├── assets/ # 静态资源
│ ├── js/ (app.js / admin.js / editor.js)
│ ├── css/ (app.css / admin.css / editor.css / theme-trae.css)
│ ├── avatar/ (随机默认头像池)
│ ├── gif/ (表情包)
│ ├── img/ (内置图片/favicon)
│ └── music/ (通知提示音)
├── install/ # 安装向导(index.php 四步流程 + schema.php 49 张表 DDL)
├── storage/ # 运行时数据(cache / logs / sessions)
└── uploads/ # 上传文件(按 年月/日 分目录)
3. 请求生命周期
以一次前台访问 `GET /thread-123.html` 为例(入口 [index.php](../index.php),引导 [bootstrap.php](../app/bootstrap.php)):
HTTP 请求
│
├─ index.php:定义 BASE_PATH / APP_START → require app/bootstrap.php
│ └─ 未安装且存在 install/ → 302 跳转安装向导
│
├─ bootstrap.php 启动序列(仅在 config/config.php 存在时):
│ 1. spl_autoload_register:按 Core→Controllers→Admin→Services→Services/Storage 顺序查找类文件
│ 2. require app/helpers.php(全局函数)
│ 3. 加载 config/config.php → $GLOBALS['__config'],define('PREFIX', 前缀)
│ 4. 错误日志指向 storage/logs/php_error.log,设定时区
│ 5. Db::init($config['db']) ← PDO 连接(异常模式 / FETCH_ASSOC / 禁用模拟预处理)
│ 6. 会话:回退 storage/sessions、cookie 名 clarabbs、HttpOnly + SameSite=Lax;
│ 注册 SessionStore 自定义处理器(读不持锁 + 写原子替换,同用户并发请求不串行;目录不可写回退默认)
│ 7. Settings::load() ← settings 表载入(文件缓存 60s + 写穿,未命中才查库)
│ 8. Plugin::boot() ← 逐个 include 已启用插件的 plugin.php(注册钩子)
│ (helpers 聚合入口在此前 require:app/helpers/ 下 13 个域文件,含 3 个顶级 Cron::register 清理任务)
│ 9. CreditRuleService::boot() ← 积分规则引擎注册业务事件钩子(表未建静默关闭)
│ 10. GeoService::boot() ← GEO:新帖推送钩子 + 每日体检 Cron(未就绪静默降级)
│ 11. list_cache_boot() ← 列表页游客缓存失效钩子(发帖/回帖 bump list_ver)
│
├─ App::run('web')
│ 1. canonical_host_redirect() ← 配置 site_url 后非主域 308 统一跳转(防会话丢失)
│ 2. GeoService::recordBotHit() ← AI 爬虫 UA 访问监控(非爬虫纳秒级返回)
│ 3. require app/routes.php ← 注册核心前台路由
│ 4. Hook::fire('routes_register', $router) ← 插件追加自有路由
│ 5. register_shutdown_function(Cron::lazyTick) ← 计划任务懒触发(shutdown 阶段必达)
│ 6. maintenance_guard() ← 维护模式拦截(503 + Retry-After;后台入口不受影响)
│ 7. $router->dispatch() ← 见下
│
├─ Router::dispatch()
│ · URI 剥离子目录前缀、/index.php、.html 伪静态后缀
│ · 逐条正则匹配 → 命中后参数写入 $GLOBALS['__params']
│ · invoke:'ThreadController@show' → (new ThreadController())->show()
│ · 未命中 → 404(wantsJson 则输出 JSON)
│
├─ Controller 构造(Controller 基类)
│ · POST 请求自动 Csrf::check()
│ · Auth::boot()(惰性解析当前用户:session uid + remember cookie 兜底)
│
├─ Action 业务逻辑
│ · 调用 Db / Auth / 服务类 / helpers 全局函数
│ · Hook::fire('thread_created'|'post_created'|…) 通知插件
│
└─ 渲染响应
· View::display('thread', $vars) → 先渲染视图为 $content,再套 layout.php 输出
· 或 json_out() 输出 JSON(自动 no-store 防缓存)
· 异常:写入 storage/logs/error_Ymd.log → 500 页 / JSON 错误
后台请求走 `admin.php` → `App::run('admin')` → `admin_routes.php`,先 `Hook::fire('admin_routes_register', $router)` 让插件注册后台页,再由 `/admin.php/{module}/{action}` 通配路由交给 `AdminRouter::dispatch()`(登录 + 管理员 + CSRF 三重守卫,详见 [04-后台管理模块](04-后台管理模块.md))。
4. 分层架构与依赖方向
┌──────────────────────────────────────────────────────┐
│ 入口层 index.php / admin.php / install/index.php │
├──────────────────────────────────────────────────────┤
│ 路由层 routes.php / admin_routes.php (+ 插件路由) │
├──────────────────────────────────────────────────────┤
│ 控制层 Controllers/*(前台) Admin/*(后台) │
│ ↑ 继承 Controller / AdminController │
├──────────────────────────────────────────────────────┤
│ 服务层 Services/*(Geo/CreditRule/Mailer/Medal/Seo/Storage)│
├──────────────────────────────────────────────────────┤
│ 核心层 Core/*(Router/Db/Auth/View/Hook/Plugin/ │
│ Settings/Request/Csrf/Validator) │
│ helpers.php 全局函数 │
├──────────────────────────────────────────────────────┤
│ 数据层 MySQL(clara_* 表 + plugin_* 插件表) │
├──────────────────────────────────────────────────────┤
│ 扩展层 content/plugins/*(只经 Hook 与 Plugin 接触核心)│
└──────────────────────────────────────────────────────┘
依赖规则:
- 核心层不依赖业务层:`Core/*` 只互相引用(如 Controller → Csrf/Auth),从不 include 控制器。
- 业务层向下依赖:控制器可用核心类、服务类、helpers;服务类只用核心类 + Db。
- 插件单向依赖核心:插件可调用一切核心 API(Db/Auth/Hook/View/Plugin::config),核心对插件只通过 `Plugin::boot()` 加载与 `Hook` 回调感知,未启用的插件代码永不执行。
- 视图层最末端:视图只用 helpers 函数 + `$vars` 数据,不做业务查询(少量展示性查询如 `avatar_html()` 内置的兜底查询除外)。
5. 关键横切机制
| 机制 | 实现位置 | 说明 |
|---|---|---|
| 自动加载 | `bootstrap.php` spl\_autoload | 按固定目录列表查找 `{ClassName}.php`,类名即文件名 |
| CSRF | `Core/Csrf.php` + `Controller` 基类 | 所有 POST 自动校验;AJAX 走 `X-CSRF-Token` 头 |
| 权限 | `Core/Auth.php` | 用户组 perms JSON 合并 + 版块级覆盖 + 版主判定 + 管理员全通 |
| 会话 | `bootstrap.php` + `Core/SessionStore.php` | cookie `clarabbs`,HttpOnly + SameSite=Lax + HTTPS 下自动 Secure(`Request::isHttps()`);登录成功 `session_regenerate_id` 防固定;自定义存储读不锁/写原子(同用户并发不串行) |
| 错误处理 | `App::run()` catch | 生产不显示错误,全部落 `storage/logs/error_Ymd.log` |
| 设置缓存 | `Core/Settings.php` | settings 表载入内存 + 文件缓存(60s TTL),`set()` 同步写库并写穿缓存 |
| 插件容错 | `Core/Hook.php` | 回调 try/catch,异常写 `plugin_error_Ymd.log`,不影响主流程 |
6. 前后端交互模式
- 传统表单为主:多数页面为服务端渲染 + POST 表单(带 `csrf_field()`),成功后 `redirect()` + flash 消息。
- 局部 AJAX 增强(`assets/js/app.js`):
- 通知轮询:`GET /notifications/poll`,15 秒一次,支持提示音与桌面提醒
- 新回复检测:`GET /thread-{id}/sync`(帖子页 10 秒 / 版块与首页 20 秒),有新回复自动刷新并保持滚动位置
- 点赞 / 收藏 / 礼物 / 删除等操作走 `fetch` + JSON
- JSON 输出约定:`json_out(array('ok' => bool, 'msg' => string, ...))`;未登录 AJAX 返回 `{'ok':false,'need_login':true}` 由前端引导登录。
- CSRF 自愈:AJAX 校验失败时服务端返回 `csrf_expired` 标记,前端自动 `GET /csrf/token` 拉取当前会话新 token 重试一次(editor.js 已内置,复杂插件 JS 可复用同协议)。
本文档随系统发布维护;如发现与当前版本不符,欢迎到社区反馈。







