Bearweb数据库

Bearweb CMS是一个数据库驱动内容管理系统。这篇文章将阐述数据库的结构与如何编写、修改、维护数据库。

--by @ Jan 24, 2026

Index

我决定自己造轮子的一个原因就是因为我用过的CMS(内容管理系统)的数据库结构都太复杂了。所以,当我写这个CMS时的目标就是创造一个结构简单、可移植、好管理的底层数据库。

Bearweb CMS是数据库驱动的,每一个模组都有自己的独立数据库。Bearweb CMS在设计时就考虑SQLite,但是也可以轻松移植到其它的关系数据库上。对于SQLite来说,每个数据库都是一个文件。因此,为每一个模组创建自己的独立数据库(文件)应该可以最大化减少拥挤。

Bearweb CMS结构非常简单。它有3个模组(站点地图、对话与交互、用户),合计3个文件内共4个数据库:

Bearweb CMS的模组都是数据库驱动的,每一个实例都代表了数据库中的一个记录,每一个实例变量都可以在数据中找到相应的值。int实例变量将以integer保存在数据库中,string变量将以text保存,arrayobject变量将以JSON格式的text保存,二进制string变量将以blob保存。

Bearweb CMS是可移植的。它在设计时就考虑使用可移植的数据库,即SQLite。因此,网站迁移只需要复制粘贴PHP文件(CMS)与数据库文件(数据)。

确保数据库文件与其目录都是可读写的。

虽然推荐使用API来修改网站,但是偷懒直接修改数据库就是又快又爽。数据库也是很方便管理的,在设计时就决定不使用外键和触发。因此,你可以直接修改数据库里面的数据来修改网站,当然:

也不用太担心。如果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
	

Bearweb_Site数据库结构

SQLite类型 默认值 解释 例子
url text, PK, Unique, Not null 无默认值 资源URL。所有的资源都通过URL进行索引。
  • 'hello/world.html' - 使用这个URL来访问该资源。
category text, Not null '' 资源分类,用于管理目的,Bearweb CMS不使用这个值。
  • 'Article' - 这个资源属于一个“Article”。
template JSON array in text, Not null '["object", "blob"]' - 对象(文件)模板,直接输出内容。 进一步处理这个资源的模板。Bearweb CMS将调用template[0](模板),然后template[0]将调用template[1](子模板)。
  • '['page-en', 'direct']' - Bearweb CMS将调用Bearweb_Site::Dir_Template目录下的page-en.php, 然后page-en.php将调用Bearweb_Site::Dir_Template目录下的page-en_direct.php
owner text, Not null ''(空字符串) - 系统所有资源。 资源所有者的用户ID(user id)。只有所有者和管理员(组0)才可以修改这个资源。
  • '' - 系统所有资源。例如自动生成的资源(例如RSS站点地图)。
  • 'John' - 用户名为“John”的资源。例如用户写的文章。
create int, Not null 0Bearweb_Site::TIME_NULL) - 无实际时间。 创建时间戳。
  • 0 - 无实际时间,例如自动生成的资源。(例如RSS站点地图)
  • 60 - 资源创建于1970-01-01 00:01:00。
modify int, Not null 0Bearweb_Site::TIME_NULL) - 无实际时间。 修改时间戳。
  • 0 - 无实际时间,例如API。
  • 60 - 资源修改于1970-01-01 00:01:00。
meta JSON object in text, Not null '{}' - 空元数据。 资源元素据。包含Bearweb CMS架构与模板使用的可选数据。
  • '{"title": "Example Title", "description": "This is an example resource."}' - 见下文
content text或null '' - 空内容。 资源内容。为最小化服务器负载,内容应该是最终输出的内容(对于网页来说,这里应该包含HTML而不是markdown)。如果为null,Bearweb将在资源目录下以url为文件名读取文件。这是为了不在数据库中保存大文件,参考我的博客详述
  • '<div><!-- 我的HTML内容 --></div>' - 一篇文章。
  • 'ABCDEFG' - 一个纯文本文件。
  • 48613FA87E... - 一个二进制文件
  • null - 文件支持资源。Bearweb CMS将在资源目录下以url为文件名读取文件。
aux JSON object in text, Not null '{}' - 空额外数据。 资源额外素据。包含模板使用的可选数据。
  • '{"lang-en": "url/en", "lang-zh": "url/zh"}' - 见下文

metaaux保存了可选信息。为这些可选信息各自单独创建数据库中表的列将会使得表很宽很不便于管理且产生大量不使用的值。因此,使用JSON保存它们。

Bearweb_Site meta可选数据

类型 解释 使用模板与例子
title string 标题 All
  • '{"title": "示例标题"}' - 页面标题为“示例标题”。
keywords string, comma-seperated words 关键字 All
  • '{"keywords": "关键字1, 关键字2, 关键字3"}' - 页面有3个关键字:“关键字1”、“关键字2”、“关键字3”。
description string 简述 All
  • '{"description": "这是一个简述。"}' - 页面简述为“这是一个简述。”。
r301 string 永久重定向至新的URL。 Core
  • '{"r301": "new/page.html"}' - 使Bearweb发送301 Moved PermanentlyHTTP头至“/new/page.html”。注意该值为绝对URL且省略开头的“/”。
r302 string 临时重定向至新的URL。 Core
  • '{"r301": "new/page.html"}' - 使Bearweb发送302 FoundHTTP头至“/new/page.html”。注意该值为绝对URL且省略开头的“/”。
robots string 告诉爬虫不要收录本资源/发现本资源的链接。 CoreAll
  • '{"r301": "noindex"}' - 不要收录本资源。
  • '{"r301": "nofollow"}' - 不要发现本资源的链接。
  • '{"r301": "noindex, nofollow"}' - 以上两项。

但是有的爬虫坏得很,说了当成耳边风。

不要使用noindexnofollownoindex, nofollow以外任何值。其它值不一定被所有爬虫都理解。

access array of number and string 如果定义,Bearweb将对该资源启用访问控制。只有用户ID和组被包含在这个数组里才能访问这个资源,其它人都会收到HTTP 403错误。注意,Bearweb CMS使用string作为用户名,int作为用户组。 Core
  • '{"access": ["Alice", 23, "Bob"]}' - 允许用户名为“Alice”和“Bob”的两位用户,以及分组23下的用户读取。
  • '{"access": []}' - 任何人都看不到。
  • 资源所有者和管理员(组0)永远都有权读写。
img string 海报图片URL。 All
  • '{"bgimg": "sun.jpg"}' - 使用“/sun.jpg”作为该页海报。注意该值为绝对URL且省略开头的“/”。
Image
  • '{"hd": "sun.thumb.jpg"}' - 在图片(Image)子模版下,使用该图为缩略图。注意该值为绝对URL且省略开头的“/”。
hd string 高清大图的URL。 Image
  • '{"hd": "sun.jpg"}' - 使用“/sun.jpg”作为该页的高清大图。注意该值为绝对URL且省略开头的“/”。
ratio number 图片比例(width / height)。 Image
  • 1.33333 - 方便浏览器在完全下载图片前就能正确排版。在生成照片墙时使用。
task API task 任务。 All
  • '{"task": "login"}' - 执行“login”任务。

上述值都为可选的。当需要使用某个值但没有定义时,Bearweb将会使用默认值。

Bearweb_Site aux可选数据

类型 解释 使用模板与例子
lang-* string 多语言资源备选语言。 All
  • '{"lang-en": "url/en", "lang-zh": "url/zh"}' - 备选语言(包括当前语言)的URL。注意该值为绝对URL且省略开头的“/”。
mime string 对象MIME类型 All
  • '{"mime": "image/png"}' - 对象模板将发送Content-Type: image/png HTTP头(PNG图片)。
  • 如果没有设定,对象模板将发送Content-Type: text/plain HTTP头(纯文本)。

上述值都为可选的。当需要使用某个值但没有定义时,Bearweb将会使用默认值。

Bearweb会话

一个交互(Transaction)指由客户端发送给服务端的一次HTTP请求和服务端返回给客户端的相应回复。

对于Bearweb CMS来说,一个请求包含了3部分:

对于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
	

Bearweb_Session.Transaction数据库结构

SQLite类型 默认值 解释 例子
id text, PK, Unique, Not null 无默认值 交互ID。
  • 'AZaz09+=' - 使用这个ID来查找一个交互。
create int, Not null -1 - 错误值,不会发生。 创建时间戳。
  • 60 - 交互创建于1970-01-01 00:01:00。该值为服务器接收到请求的实际时间,而非客户端发送请求的时间。
ip text, Not null '' 客户端IP地址与端口,可以是IPv4或IPv6地址。
  • '1.2.3.4:10086' - 客户端IP地址与端口。
  • '1234:5678:90ab:cdef:ff00:5599:abcd:1234:12345' - 客户端IP地址与端口。
url text, Not null '' 请求的资源或API的URL。
  • 'hello/world.html' - 请求的资源或API的URL。
status int, Not null 0 - 错误值,致命错误发生。 HTTP状态码。
  • 200 - 服务器返回了一个HTTP 200 OK到客户端。
  • 404 - 服务器返回了一个HTTP 404 Not Found到客户端。
  • 500 - 服务器返回了一个HTTP 500 Internal Server Error到客户端。
  • 0 - 致命错误发生。PHP脚本在写入返回的HTTP状态码前就退出了。
time int, Not null -1 - 错误值,致命错误发生。 服务器处理该请求花费的时间(单位:微秒)。注意,这包括服务器等待数据传输的时间(对于文件,这将非常大,甚至会几十秒)。该值通过(microtime(true) - $_SERVER["REQUEST_TIME_FLOAT"]) * 1e6得到。
  • 25000 - 服务器花费25毫秒。
  • -1 - 致命错误发生。PHP脚本在写入处理时间前就退出了。
memory int, Not null -1 - 错误值,致命错误发生。 服务器处理该请求占用的内存(单位:KiB)。该值通过memory_get_peak_usage(false) / 1024得到。
  • 1024 - 服务器占用1 MiB内存。
  • -1 - 致命错误发生。PHP脚本在写入内存用量前就退出了。
session text, Not null '' 对应会话ID。
  • 'zaZA90=+' - 该ID用于查找对应会话ID。
log text, or null '' - 空log。 调试用。使用$BW->log('Hello world');来打一行log。
  • 'Hello\nStart\nDone!' - 一些log。
  • '' - 没有log。或者也可能,
  • '' - 致命错误发生。PHP脚本在写入log前就退出了。

Bearweb_Session.Session数据库结构

SQLite类型 默认值 解释 例子
id text, PK, Unique, Not null 无默认值 会话ID。
  • 'zaZA90=+' - 使用这个ID来查找一个会话。
create int, Not null -1 - 错误值,不会发生。 创建时间戳。
  • 60 - 会话创建于1970-01-01 00:01:00。该值为服务器接收到请求的实际时间,而非客户端发送请求的时间。
lastuse int, Not null -1 - 错误值,不会发生。 最后使用时间戳。
  • 120 - 会话最后使用于1970-01-01 00:02:00。该值为服务器接收到请求的实际时间,而非客户端发送请求的时间。
user text, Not null '' - 未绑定用户(游客)。 会话绑定的用户的ID。
  • '' - 游客会话。
  • 'John' - 绘画绑定为用户“John”。
key text, Not null '' - 错误值。 会话验证钥匙。
  • '90=+zaZA' - 会话验证钥匙。

会话与交互数据库维护

使用这个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
	

Bearweb_User数据库结构

SQLite类型 默认值 解释 例子
id text, PK, Unique, Not null 无默认值 用户ID。
  • 'John' - 使用这个ID来查找一个用户。
name text, Not null '' 用户昵称。这个昵称可以重名。
  • 'John Doe' - 用户的昵称。
salt text, Not null ':' - 非法值。 密码用盐。
  • 'AZaz09+=' - 密码哈希加盐。
password text, Not null ':' - 非法值。 加盐哈希后的32字节(256-bit)密码。
  • 'zaZA90=+' - 加盐哈希后的密码。
registertime int, Not null 0 创建帐号时间戳。
  • 60 - 帐号创建于1970-01-01 00:01:00。该值为服务器接收到请求的实际时间,而非客户端发送请求的时间。
lastactive int, Not null 0 最后活跃时间戳。
  • 120 - 帐号最后活跃于1970-01-01 00:02:00。该值为服务器接收到请求的实际时间,而非客户端发送请求的时间。
group JSON array in text, Not null '[]' - 无分组。 用户的分组。使用int作为组ID。组0为“管理员”。
  • '[0, 1]' - 用户在管理员分组和“分组1”。
  • '[114]' - 用户在“分组114”。
data JSON object in text, Not null '{}' - 空数据。 用户的其它数据。
  • '{"key1": "value 1", "key2": 114514}' - 用户数据。