数据库系统
mdView 支持三种数据库:SQLite(默认)、MySQL、SQL Server。通过抽象方言层自动适配不同数据库的 SQL 语法差异。
架构
config.php → 选择数据库类型,填写连接信息
↓
Database.php → createDialect() 创建对应的方言实例
↓
SchemaDialect (抽象) → 统一的 SQL 生成接口
↓
SQLiteDialect → ON CONFLICT / PRAGMA / AUTOINCREMENT
MySQLDialect → ON DUPLICATE KEY / INFORMATION_SCHEMA / AUTO_INCREMENT
SQLServerDialect → MERGE / NVARCHAR(MAX) / IDENTITY
建表、迁移、索引、upsert 等所有 SQL 均由方言动态生成,业务代码不感知底层数据库差异。
配置
配置文件:config.php
return [
'db' => [
// 数据库类型:sqlite | mysql | sqlserver
'type' => 'sqlite',
// SQLite 配置
'sqlite' => [
'path' => 'data/mdview.db', // 相对于项目根目录
],
// MySQL 配置
'mysql' => [
'host' => '127.0.0.1',
'port' => 3306,
'database' => 'mdview',
'username' => 'root',
'password' => '',
'charset' => 'utf8mb4',
],
// SQL Server 配置
'sqlserver' => [
'host' => '127.0.0.1',
'port' => 1433,
'database' => 'mdview',
'username' => 'sa',
'password' => '',
],
],
];
如果 config.php 不存在,系统自动使用 SQLite 默认配置,数据库文件位于 data/mdview.db。
切换数据库
config.php,修改 type 为目标数据库类型CREATE DATABASE mdview ...)方言抽象层
SchemaDialect 基类
定义了所有方言必须实现的方法:
三种方言对比
Database 类
单例模式,通过 Database::getInstance() 获取实例。
初始化流程
new Database()
├── loadConfig() → 读取 config.php,不存在则默认 SQLite
├── createDialect(type) → 根据 type 创建 SQLiteDialect / MySQLDialect / SQLServerDialect
├── SQLite: 确保 data/ 目录存在
├── dialect->buildDsn() → 构造 PDO DSN 并建立连接
├── dialect->initConnection() → 方言特定的连接初始化
├── initTables() → 建表(SQL 通过 buildSQL() 做类型替换)
│ ├── users
│ ├── documents
│ ├── settings
│ ├── roles
│ ├── menus
│ ├── footer_menus
│ ├── ad_categories / ad_placements / ad_items
│ ├── doc_shares
│ ├── reading_logs
│ └── migrate() → 为旧数据库添加新列(兼容升级)
└── seedDefaultData() → 填充默认数据
关键方法
buildSQL() 类型替换
initTables() 中的建表语句以 SQLite 语法编写,通过 buildSQL() 动态替换类型关键字:
原始 SQL 替换结果(以 SQL Server 为例)
────────────────────────────────────────────────────────────────
INTEGER PRIMARY KEY AUTOINCREMENT → INT IDENTITY(1,1) PRIMARY KEY
TEXT → NVARCHAR(MAX)
DATETIME → DATETIME2
替换仅作用于列类型声明位置,不影响列名或字符串常量。
数据表结构 DDL
以下列出系统全部 10 张表的建表语句,按三种数据库分别给出。
1. users — 用户
SQLite
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT UNIQUE NOT NULL,
password TEXT NOT NULL,
role TEXT NOT NULL DEFAULT 'member',
role_id INTEGER DEFAULT 0,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
MySQL
CREATE TABLE IF NOT EXISTS users (
id INT AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(255) NOT NULL UNIQUE,
password VARCHAR(255) NOT NULL,
role VARCHAR(50) NOT NULL DEFAULT 'member',
role_id INT DEFAULT 0,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
SQL Server
IF NOT EXISTS (SELECT 1 FROM sys.tables WHERE name = 'users')
CREATE TABLE users (
id INT IDENTITY(1,1) PRIMARY KEY,
username NVARCHAR(255) NOT NULL UNIQUE,
password NVARCHAR(255) NOT NULL,
role NVARCHAR(50) NOT NULL DEFAULT 'member',
role_id INT DEFAULT 0,
created_at DATETIME2 DEFAULT GETDATE()
);
2. documents — 文档
SQLite
CREATE TABLE IF NOT EXISTS documents (
id INTEGER PRIMARY KEY AUTOINCREMENT,
parent_id INTEGER DEFAULT 0,
name TEXT NOT NULL,
path TEXT NOT NULL DEFAULT '',
type TEXT NOT NULL DEFAULT 'file',
sort_order INTEGER DEFAULT 0,
level INTEGER DEFAULT 0,
view_count INTEGER DEFAULT 0,
is_recommended INTEGER DEFAULT 0,
recommend_sort INTEGER DEFAULT 0,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
MySQL
CREATE TABLE IF NOT EXISTS documents (
id INT AUTO_INCREMENT PRIMARY KEY,
parent_id INT DEFAULT 0,
name VARCHAR(255) NOT NULL,
path VARCHAR(500) NOT NULL DEFAULT '',
type VARCHAR(20) NOT NULL DEFAULT 'file',
sort_order INT DEFAULT 0,
level INT DEFAULT 0,
view_count INT DEFAULT 0,
is_recommended TINYINT DEFAULT 0,
recommend_sort INT DEFAULT 0,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
SQL Server
IF NOT EXISTS (SELECT 1 FROM sys.tables WHERE name = 'documents')
CREATE TABLE documents (
id INT IDENTITY(1,1) PRIMARY KEY,
parent_id INT DEFAULT 0,
name NVARCHAR(255) NOT NULL,
path NVARCHAR(500) NOT NULL DEFAULT '',
type NVARCHAR(20) NOT NULL DEFAULT 'file',
sort_order INT DEFAULT 0,
level INT DEFAULT 0,
view_count INT DEFAULT 0,
is_recommended TINYINT DEFAULT 0,
recommend_sort INT DEFAULT 0,
created_at DATETIME2 DEFAULT GETDATE(),
updated_at DATETIME2 DEFAULT GETDATE()
);
3. settings — 配置
SQLite
CREATE TABLE IF NOT EXISTS settings (
key TEXT PRIMARY KEY,
value TEXT
);
MySQL
CREATE TABLE IF NOT EXISTS settings (
`key` VARCHAR(255) PRIMARY KEY,
value TEXT
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
SQL Server
IF NOT EXISTS (SELECT 1 FROM sys.tables WHERE name = 'settings')
CREATE TABLE settings (
[key] NVARCHAR(255) PRIMARY KEY,
value NVARCHAR(MAX)
);
4. roles — 角色
SQLite
CREATE TABLE IF NOT EXISTS roles (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
slug TEXT NOT NULL UNIQUE,
description TEXT DEFAULT '',
permissions TEXT DEFAULT '[]',
level INTEGER DEFAULT 0,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
MySQL
CREATE TABLE IF NOT EXISTS roles (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(255) NOT NULL,
slug VARCHAR(100) NOT NULL UNIQUE,
description TEXT DEFAULT '',
permissions TEXT DEFAULT '[]',
level INT DEFAULT 0,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
SQL Server
IF NOT EXISTS (SELECT 1 FROM sys.tables WHERE name = 'roles')
CREATE TABLE roles (
id INT IDENTITY(1,1) PRIMARY KEY,
name NVARCHAR(255) NOT NULL,
slug NVARCHAR(100) NOT NULL UNIQUE,
description NVARCHAR(MAX) DEFAULT '',
permissions NVARCHAR(MAX) DEFAULT '[]',
level INT DEFAULT 0,
created_at DATETIME2 DEFAULT GETDATE(),
updated_at DATETIME2 DEFAULT GETDATE()
);
5. menus — 导航菜单
SQLite
CREATE TABLE IF NOT EXISTS menus (
id INTEGER PRIMARY KEY AUTOINCREMENT,
parent_id INTEGER DEFAULT 0,
name TEXT NOT NULL,
url TEXT NOT NULL DEFAULT '',
icon TEXT DEFAULT '',
target TEXT DEFAULT '_self',
sort_order INTEGER DEFAULT 0,
is_active INTEGER DEFAULT 1,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
MySQL
CREATE TABLE IF NOT EXISTS menus (
id INT AUTO_INCREMENT PRIMARY KEY,
parent_id INT DEFAULT 0,
name VARCHAR(255) NOT NULL,
url VARCHAR(500) NOT NULL DEFAULT '',
icon VARCHAR(255) DEFAULT '',
target VARCHAR(20) DEFAULT '_self',
sort_order INT DEFAULT 0,
is_active TINYINT DEFAULT 1,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
SQL Server
IF NOT EXISTS (SELECT 1 FROM sys.tables WHERE name = 'menus')
CREATE TABLE menus (
id INT IDENTITY(1,1) PRIMARY KEY,
parent_id INT DEFAULT 0,
name NVARCHAR(255) NOT NULL,
url NVARCHAR(500) NOT NULL DEFAULT '',
icon NVARCHAR(255) DEFAULT '',
target NVARCHAR(20) DEFAULT '_self',
sort_order INT DEFAULT 0,
is_active TINYINT DEFAULT 1,
created_at DATETIME2 DEFAULT GETDATE(),
updated_at DATETIME2 DEFAULT GETDATE()
);
6. footer_menus — 页脚菜单
SQLite
CREATE TABLE IF NOT EXISTS footer_menus (
id INTEGER PRIMARY KEY AUTOINCREMENT,
parent_id INTEGER DEFAULT 0,
name TEXT NOT NULL,
url TEXT NOT NULL DEFAULT '',
target TEXT DEFAULT '_self',
sort_order INTEGER DEFAULT 0,
is_active INTEGER DEFAULT 1,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
MySQL
CREATE TABLE IF NOT EXISTS footer_menus (
id INT AUTO_INCREMENT PRIMARY KEY,
parent_id INT DEFAULT 0,
name VARCHAR(255) NOT NULL,
url VARCHAR(500) NOT NULL DEFAULT '',
target VARCHAR(20) DEFAULT '_self',
sort_order INT DEFAULT 0,
is_active TINYINT DEFAULT 1,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
SQL Server
IF NOT EXISTS (SELECT 1 FROM sys.tables WHERE name = 'footer_menus')
CREATE TABLE footer_menus (
id INT IDENTITY(1,1) PRIMARY KEY,
parent_id INT DEFAULT 0,
name NVARCHAR(255) NOT NULL,
url NVARCHAR(500) NOT NULL DEFAULT '',
target NVARCHAR(20) DEFAULT '_self',
sort_order INT DEFAULT 0,
is_active TINYINT DEFAULT 1,
created_at DATETIME2 DEFAULT GETDATE(),
updated_at DATETIME2 DEFAULT GETDATE()
);
7. ad_categories — 广告分类
SQLite
CREATE TABLE IF NOT EXISTS ad_categories (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
slug TEXT NOT NULL UNIQUE,
description TEXT DEFAULT '',
sort_order INTEGER DEFAULT 0,
is_active INTEGER DEFAULT 1,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
MySQL
CREATE TABLE IF NOT EXISTS ad_categories (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(255) NOT NULL,
slug VARCHAR(100) NOT NULL UNIQUE,
description TEXT DEFAULT '',
sort_order INT DEFAULT 0,
is_active TINYINT DEFAULT 1,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
SQL Server
IF NOT EXISTS (SELECT 1 FROM sys.tables WHERE name = 'ad_categories')
CREATE TABLE ad_categories (
id INT IDENTITY(1,1) PRIMARY KEY,
name NVARCHAR(255) NOT NULL,
slug NVARCHAR(100) NOT NULL UNIQUE,
description NVARCHAR(MAX) DEFAULT '',
sort_order INT DEFAULT 0,
is_active TINYINT DEFAULT 1,
created_at DATETIME2 DEFAULT GETDATE(),
updated_at DATETIME2 DEFAULT GETDATE()
);
8. ad_placements — 广告位
SQLite
CREATE TABLE IF NOT EXISTS ad_placements (
id INTEGER PRIMARY KEY AUTOINCREMENT,
category_id INTEGER NOT NULL DEFAULT 0,
name TEXT NOT NULL,
slot_key TEXT NOT NULL UNIQUE,
description TEXT DEFAULT '',
max_items INTEGER DEFAULT 1,
sort_order INTEGER DEFAULT 0,
is_active INTEGER DEFAULT 1,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
MySQL
CREATE TABLE IF NOT EXISTS ad_placements (
id INT AUTO_INCREMENT PRIMARY KEY,
category_id INT NOT NULL DEFAULT 0,
name VARCHAR(255) NOT NULL,
slot_key VARCHAR(100) NOT NULL UNIQUE,
description TEXT DEFAULT '',
max_items INT DEFAULT 1,
sort_order INT DEFAULT 0,
is_active TINYINT DEFAULT 1,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
SQL Server
IF NOT EXISTS (SELECT 1 FROM sys.tables WHERE name = 'ad_placements')
CREATE TABLE ad_placements (
id INT IDENTITY(1,1) PRIMARY KEY,
category_id INT NOT NULL DEFAULT 0,
name NVARCHAR(255) NOT NULL,
slot_key NVARCHAR(100) NOT NULL UNIQUE,
description NVARCHAR(MAX) DEFAULT '',
max_items INT DEFAULT 1,
sort_order INT DEFAULT 0,
is_active TINYINT DEFAULT 1,
created_at DATETIME2 DEFAULT GETDATE(),
updated_at DATETIME2 DEFAULT GETDATE()
);
9. ad_items — 广告项
SQLite
CREATE TABLE IF NOT EXISTS ad_items (
id INTEGER PRIMARY KEY AUTOINCREMENT,
placement_id INTEGER NOT NULL DEFAULT 0,
title TEXT NOT NULL DEFAULT '',
ad_type TEXT NOT NULL DEFAULT 'image',
content TEXT DEFAULT '',
link_url TEXT DEFAULT '',
link_target TEXT DEFAULT '_self',
sort_order INTEGER DEFAULT 0,
is_active INTEGER DEFAULT 1,
start_time DATETIME DEFAULT NULL,
end_time DATETIME DEFAULT NULL,
views INTEGER DEFAULT 0,
clicks INTEGER DEFAULT 0,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
MySQL
CREATE TABLE IF NOT EXISTS ad_items (
id INT AUTO_INCREMENT PRIMARY KEY,
placement_id INT NOT NULL DEFAULT 0,
title VARCHAR(255) NOT NULL DEFAULT '',
ad_type VARCHAR(50) NOT NULL DEFAULT 'image',
content TEXT DEFAULT '',
link_url VARCHAR(500) DEFAULT '',
link_target VARCHAR(20) DEFAULT '_self',
sort_order INT DEFAULT 0,
is_active TINYINT DEFAULT 1,
start_time DATETIME DEFAULT NULL,
end_time DATETIME DEFAULT NULL,
views INT DEFAULT 0,
clicks INT DEFAULT 0,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
SQL Server
IF NOT EXISTS (SELECT 1 FROM sys.tables WHERE name = 'ad_items')
CREATE TABLE ad_items (
id INT IDENTITY(1,1) PRIMARY KEY,
placement_id INT NOT NULL DEFAULT 0,
title NVARCHAR(255) NOT NULL DEFAULT '',
ad_type NVARCHAR(50) NOT NULL DEFAULT 'image',
content NVARCHAR(MAX) DEFAULT '',
link_url NVARCHAR(500) DEFAULT '',
link_target NVARCHAR(20) DEFAULT '_self',
sort_order INT DEFAULT 0,
is_active TINYINT DEFAULT 1,
start_time DATETIME2 DEFAULT NULL,
end_time DATETIME2 DEFAULT NULL,
views INT DEFAULT 0,
clicks INT DEFAULT 0,
created_at DATETIME2 DEFAULT GETDATE(),
updated_at DATETIME2 DEFAULT GETDATE()
);
10. doc_shares — 文档分享
SQLite
CREATE TABLE IF NOT EXISTS doc_shares (
id INTEGER PRIMARY KEY AUTOINCREMENT,
document_id INTEGER NOT NULL,
share_token TEXT UNIQUE NOT NULL,
share_type TEXT NOT NULL DEFAULT 'public',
password_hash TEXT DEFAULT NULL,
expire_days INTEGER DEFAULT NULL,
access_count INTEGER DEFAULT 0,
created_at TEXT NOT NULL DEFAULT '',
is_active INTEGER DEFAULT 1
);
MySQL
CREATE TABLE IF NOT EXISTS doc_shares (
id INT AUTO_INCREMENT PRIMARY KEY,
document_id INT NOT NULL,
share_token VARCHAR(64) NOT NULL UNIQUE,
share_type VARCHAR(20) NOT NULL DEFAULT 'public',
password_hash VARCHAR(255) DEFAULT NULL,
expire_days INT DEFAULT NULL,
access_count INT DEFAULT 0,
created_at VARCHAR(25) NOT NULL DEFAULT '',
is_active TINYINT DEFAULT 1
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
SQL Server
IF NOT EXISTS (SELECT 1 FROM sys.tables WHERE name = 'doc_shares')
CREATE TABLE doc_shares (
id INT IDENTITY(1,1) PRIMARY KEY,
document_id INT NOT NULL,
share_token NVARCHAR(64) NOT NULL UNIQUE,
share_type NVARCHAR(20) NOT NULL DEFAULT 'public',
password_hash NVARCHAR(255) DEFAULT NULL,
expire_days INT DEFAULT NULL,
access_count INT DEFAULT 0,
created_at NVARCHAR(25) NOT NULL DEFAULT '',
is_active TINYINT DEFAULT 1
);
11. reading_logs — 阅读日志
SQLite
CREATE TABLE IF NOT EXISTS reading_logs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL DEFAULT 0,
doc_path TEXT NOT NULL DEFAULT '',
ip TEXT NOT NULL DEFAULT '',
read_at TEXT NOT NULL DEFAULT ''
);
CREATE INDEX IF NOT EXISTS idx_rl_user_date ON reading_logs(user_id, read_at);
MySQL
CREATE TABLE IF NOT EXISTS reading_logs (
id INT AUTO_INCREMENT PRIMARY KEY,
user_id INT NOT NULL DEFAULT 0,
doc_path VARCHAR(500) NOT NULL DEFAULT '',
ip VARCHAR(45) NOT NULL DEFAULT '',
read_at VARCHAR(25) NOT NULL DEFAULT ''
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE INDEX IF NOT EXISTS idx_rl_user_date ON reading_logs(user_id, read_at);
SQL Server
IF NOT EXISTS (SELECT 1 FROM sys.tables WHERE name = 'reading_logs')
CREATE TABLE reading_logs (
id INT IDENTITY(1,1) PRIMARY KEY,
user_id INT NOT NULL DEFAULT 0,
doc_path NVARCHAR(500) NOT NULL DEFAULT '',
ip NVARCHAR(45) NOT NULL DEFAULT '',
read_at NVARCHAR(25) NOT NULL DEFAULT ''
);
IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = 'idx_rl_user_date' AND object_id = OBJECT_ID('reading_logs'))
CREATE INDEX idx_rl_user_date ON reading_logs(user_id, read_at);
数据类型映射速查
说明
- MySQL 的TEXT类型不能指定默认值(除 NULL 外),需要改用VARCHAR(N)
- SQL Server 的NVARCHAR(MAX)同样不能有默认值,需要改用NVARCHAR(N)
- MySQL 的TEXT列不能直接作为主键,需指定前缀长度或改用VARCHAR
- SQL Server 中DEFAULT CURRENT_TIMESTAMP可替换为DEFAULT GETDATE()(等效)
- 运行时 DDL 通过方言的 buildSQL() 自动转换类型关键字,手动执行 DDL 时请参照上表调整
如何在模型中使用方言
业务模型需要执行 upsert 操作时,通过 Database::getInstance()->getDialect() 获取方言实例:
// SettingModel::set() 示例
$db = Database::getInstance();
$sql = $db->getDialect()->upsertSQL('settings', ['key', 'value'], 'key');
$db->getPdo()->prepare($sql)->execute([$key, $value]);
扩展新数据库
如需支持新数据库(如 PostgreSQL),只需:
core/db/ 下创建 PostgreDialect.php,继承 SchemaDialectDatabase::createDialect() 中添加 case 分支config.php 中添加对应的配置段注意事项
config.php 不是必需文件,系统会自动回退pdo_sqlsrv PDO 驱动? PDO 占位符,不受数据库差异影响