Bearweb数据库
Bearweb CMS是一个数据库驱动内容管理系统。这篇文章将阐述数据库的结构与如何编写、修改、维护数据库。
CMS, 内容管理系统, 数据库, 数据库管理系统, SQLite, 数据库驱动, 站点地图, 会话控制, 用户管理
--by @ Jan 24, 2026Index
我决定自己造轮子的一个原因就是因为我用过的CMS(内容管理系统)的数据库结构都太复杂了。所以,当我写这个CMS时的目标就是创造一个结构简单、可移植、好管理的底层数据库。
Bearweb CMS是数据库驱动的,每一个模组都有自己的独立数据库。Bearweb CMS在设计时就考虑SQLite,但是也可以轻松移植到其它的关系数据库上。对于SQLite来说,每个数据库都是一个文件。因此,为每一个模组创建自己的独立数据库(文件)应该可以最大化减少拥挤。
Bearweb CMS结构非常简单。它有3个模组(站点地图、对话与交互、用户),合计3个文件内共4个数据库:
Sitemap(站点地图) - 每一个记录对应一个资源,可以是一张网页、一个文件、或一个API接口。Session(对话) - 一个会话代表一连串同一个客户与服务器的交互。Transaction(交互) - 一个交互代表一次客户到服务器的HTTP请求和服务器到客户的HTTP回复。User(用户) - 每一个记录对应一个注册的用户。
Bearweb CMS的模组都是数据库驱动的,每一个实例都代表了数据库中的一个记录,每一个实例变量都可以在数据中找到相应的值。int实例变量将以integer保存在数据库中,string变量将以text保存,array与object变量将以JSON格式的text保存,二进制string变量将以blob保存。
Bearweb CMS是可移植的。它在设计时就考虑使用可移植的数据库,即SQLite。因此,网站迁移只需要复制粘贴PHP文件(CMS)与数据库文件(数据)。
确保数据库文件与其目录都是可读写的。
虽然推荐使用API来修改网站,但是偷懒直接修改数据库就是又快又爽。数据库也是很方便管理的,在设计时就决定不使用外键和触发。因此,你可以直接修改数据库里面的数据来修改网站,当然:
- 数据类型不能被修改。比如,
Sitemap.create应该包含int类型的时间戳,那么修改时也应该使用int类型的时间戳(如0),而不要改成string(如1970-01-01 00:00:00)。 - 不要破坏JSON格式。Bearweb CMS大量使用JSON来保存键值数据,例如
Sitemap.meta和User.group。如果JSON格式被破坏了,Bearweb CMS就无法正确解析这个数据了。 - 我推荐使用DB browser for SQLite来管理数据库。这个软件可以防止错误修改变量类型或JSON格式,还可以高亮语法、格式/压缩(移除格式空格)JSON。
也不用太担心。如果Brarweb CMS发现了错误数据类型或无法解析时,会使用默认值。
Bearweb站点地图
请求一个网页、一个文件(如图片),或是其它资源,说白了就是从数据库中找到该资源,判断读写权限,并进行一些数据处理。该数据库叫做站点地图(Sitemap)。
CREATE TABLE "Sitemap" (
"url" TEXT NOT NULL UNIQUE,
"category" TEXT NOT NULL DEFAULT '',
"template" TEXT NOT NULL DEFAULT '["object", "blob"]',
"owner" TEXT NOT NULL DEFAULT '',
"create" INTEGER NOT NULL DEFAULT 0,
"modify" INTEGER NOT NULL DEFAULT 0,
"meta" TEXT NOT NULL DEFAULT '{}',
"content" BLOB DEFAULT '',
"aux" TEXT NOT NULL DEFAULT '{}',
PRIMARY KEY("url")
) WITHOUT ROWID
| 列 | SQLite类型 | 默认值 | 解释 | 例子 |
|---|---|---|---|---|
| url | text, PK, Unique, Not null | 无默认值 | 资源URL。所有的资源都通过URL进行索引。 |
|
| category | text, Not null | '' |
资源分类,用于管理目的,Bearweb CMS不使用这个值。 |
|
| template | JSON array in text, Not null | '["object", "blob"]' - 对象(文件)模板,直接输出内容。 |
进一步处理这个资源的模板。Bearweb CMS将调用template[0](模板),然后template[0]将调用template[1](子模板)。 |
|
| owner | text, Not null | ''(空字符串) - 系统所有资源。 |
资源所有者的用户ID(user id)。只有所有者和管理员(组0)才可以修改这个资源。 |
|
| create | int, Not null | 0(Bearweb_Site::TIME_NULL) - 无实际时间。 |
创建时间戳。 |
|
| modify | int, Not null | 0(Bearweb_Site::TIME_NULL) - 无实际时间。 |
修改时间戳。 |
|
| meta | JSON object in text, Not null | '{}' - 空元数据。 |
资源元素据。包含Bearweb CMS架构与模板使用的可选数据。 |
|
| content | text或null | '' - 空内容。 |
资源内容。为最小化服务器负载,内容应该是最终输出的内容(对于网页来说,这里应该包含HTML而不是markdown)。如果为null,Bearweb将在资源目录下以url为文件名读取文件。这是为了不在数据库中保存大文件,参考我的博客详述。 |
|
| aux | JSON object in text, Not null | '{}' - 空额外数据。 |
资源额外素据。包含模板使用的可选数据。 |
|
meta和aux保存了可选信息。为这些可选信息各自单独创建数据库中表的列将会使得表很宽很不便于管理且产生大量不使用的值。因此,使用JSON保存它们。
| 键 | 类型 | 解释 | 使用模板与例子 |
|---|---|---|---|
| title | string | 标题 |
All
|
| keywords | string, comma-seperated words | 关键字 |
All
|
| description | string | 简述 |
All
|
| r301 | string | 永久重定向至新的URL。 |
Core
|
| r302 | string | 临时重定向至新的URL。 |
Core
|
| robots | string | 告诉爬虫不要收录本资源/发现本资源的链接。 |
CoreAll
但是有的爬虫坏得很,说了当成耳边风。 不要使用 |
| access | array of number and string | 如果定义,Bearweb将对该资源启用访问控制。只有用户ID和组被包含在这个数组里才能访问这个资源,其它人都会收到HTTP 403错误。注意,Bearweb CMS使用string作为用户名,int作为用户组。 |
Core
|
| img | string | 海报图片URL。 |
All
|
| hd | string | 高清大图的URL。 |
Image
|
| ratio | number | 图片比例(width / height)。 |
Image
|
| task | API task | 任务。 |
All
|
上述值都为可选的。当需要使用某个值但没有定义时,Bearweb将会使用默认值。
| 键 | 类型 | 解释 | 使用模板与例子 |
|---|---|---|---|
| lang-* | string | 多语言资源备选语言。 |
All
|
| mime | string | 对象MIME类型 |
All
|
上述值都为可选的。当需要使用某个值但没有定义时,Bearweb将会使用默认值。
Bearweb会话
一个交互(Transaction)指由客户端发送给服务端的一次HTTP请求和服务端返回给客户端的相应回复。
对于Bearweb CMS来说,一个请求包含了3部分:
- URL:每个请求都必须包含一个URL(HTTP也规定了这一点,HTTP请求的第一行永远是
METHOD URL VERSION,比如GET /bearweb-resource-optimization/zh HTTP/1.1)。这个信息用来向服务器表示该请求的目标资源(或API)。 - 会话令牌:HTTP是无记忆的,也就是说,服务器并不知道是谁发送的请求。为了区分用户,服务器将在第一次交互时生成一个令牌(以cookie的形式)。客户端在接下来的交互中应该包含这个令牌以区分用户。如果客户端禁用了cookie或是选择不发送会话令牌cookie,服务器将会生成一个新的令牌。
- 数据:会话可以包括一些数据。主要用于API。=
对于Bearweb CMS来说,一个会话(Session)包含了来自同一个客户端的请求。
- 如上文所说,使用会话令牌区分交互所属的会话。
Bearweb CMS使用两个数据库来记录交互与会话。
CREATE TABLE "Transaction" (
"id" TEXT NOT NULL UNIQUE,
"create" INTEGER NOT NULL DEFAULT -1,
"ip" TEXT NOT NULL DEFAULT '',
"url" TEXT NOT NULL DEFAULT '',
"status" INTEGER NOT NULL DEFAULT 0,
"time" INTEGER NOT NULL DEFAULT -1,
"memory" INTEGER NOT NULL DEFAULT -1,
"session" TEXT NOT NULL DEFAULT '',
"log" TEXT NOT NULL DEFAULT '',
PRIMARY KEY("id")
) WITHOUT ROWID,STRICT
CREATE TABLE "Session" (
"id" TEXT NOT NULL UNIQUE,
"create" INTEGER NOT NULL DEFAULT -1,
"lastuse" INTEGER NOT NULL DEFAULT -1,
"user" TEXT NOT NULL DEFAULT '',
"key" TEXT NOT NULL DEFAULT '',
PRIMARY KEY("id")
) WITHOUT ROWID,STRICT
| 列 | SQLite类型 | 默认值 | 解释 | 例子 |
|---|---|---|---|---|
| id | text, PK, Unique, Not null | 无默认值 | 交互ID。 |
|
| create | int, Not null | -1 - 错误值,不会发生。 |
创建时间戳。 |
|
| ip | text, Not null | '' |
客户端IP地址与端口,可以是IPv4或IPv6地址。 |
|
| url | text, Not null | '' |
请求的资源或API的URL。 |
|
| status | int, Not null | 0 - 错误值,致命错误发生。 |
HTTP状态码。 |
|
| time | int, Not null | -1 - 错误值,致命错误发生。 |
服务器处理该请求花费的时间(单位:微秒)。注意,这包括服务器等待数据传输的时间(对于文件,这将非常大,甚至会几十秒)。该值通过(microtime(true) - $_SERVER["REQUEST_TIME_FLOAT"]) * 1e6得到。 |
|
| memory | int, Not null | -1 - 错误值,致命错误发生。 |
服务器处理该请求占用的内存(单位:KiB)。该值通过memory_get_peak_usage(false) / 1024得到。 |
|
| session | text, Not null | '' |
对应会话ID。 |
|
| log | text, or null | '' - 空log。 |
调试用。使用$BW->log('Hello world');来打一行log。 |
|
| 列 | SQLite类型 | 默认值 | 解释 | 例子 |
|---|---|---|---|---|
| id | text, PK, Unique, Not null | 无默认值 | 会话ID。 |
|
| create | int, Not null | -1 - 错误值,不会发生。 |
创建时间戳。 |
|
| lastuse | int, Not null | -1 - 错误值,不会发生。 |
最后使用时间戳。 |
|
| user | text, Not null | '' - 未绑定用户(游客)。 |
会话绑定的用户的ID。 |
|
| key | text, Not null | '' - 错误值。 |
会话验证钥匙。 |
|
会话与交互数据库维护
使用这个view来查看交互和相应的会话:
CREATE VIEW `Transaction_View` AS
select all
t.`id`,
datetime(t.`create`, 'unixepoch') as `create`,
t.`ip`,
t.`url`,
t.`status`,
t.`time` as `time (us)`,
t.`memory` as `memory (kB)`,
t.`log`,
t.`session`,
datetime(s.`create`, 'unixepoch') as `firstuse`,
datetime(s.`lastuse`, 'unixepoch') as `lastuse`,
s.`user`,
s.`key`
from `Transaction` t join `Session` s where t.`session` = s.`id`
order by `create` desc
会话与交互数据库将的体积随时间而增长。你可以定期清理过期会话和陈旧交互:
delete from `Session` where strftime('%s', 'now') - `lastuse` > 30 * 24 * 3600;
delete from `Transaction` where strftime('%s', 'now') - `create` > 30 * 24 * 3600;
vacuum;
Bearweb用户
Bearweb使用数据库来保存(User)用户信息。
CREATE TABLE "User" (
"id" TEXT NOT NULL UNIQUE,
"name" TEXT NOT NULL DEFAULT '',
"salt" TEXT NOT NULL DEFAULT ':',
"password" TEXT NOT NULL DEFAULT ':',
"registertime" INTEGER NOT NULL DEFAULT 0,
"lastactive" INTEGER NOT NULL DEFAULT 0,
"group" TEXT NOT NULL DEFAULT '{}',
"data" TEXT NOT NULL DEFAULT '{}',
PRIMARY KEY("id")
) WITHOUT ROWID,STRICT
| 列 | SQLite类型 | 默认值 | 解释 | 例子 |
|---|---|---|---|---|
| id | text, PK, Unique, Not null | 无默认值 | 用户ID。 |
|
| name | text, Not null | '' |
用户昵称。这个昵称可以重名。 |
|
| salt | text, Not null | ':' - 非法值。 |
密码用盐。 |
|
| password | text, Not null | ':' - 非法值。 |
加盐哈希后的32字节(256-bit)密码。 |
|
| registertime | int, Not null | 0 |
创建帐号时间戳。 |
|
| lastactive | int, Not null | 0 |
最后活跃时间戳。 |
|
| group | JSON array in text, Not null | '[]' - 无分组。 |
用户的分组。使用int作为组ID。组0为“管理员”。 |
|
| data | JSON object in text, Not null | '{}' - 空数据。 |
用户的其它数据。 |
|