mdView 文档
欢迎使用 mdView 文档系统
👁11 次阅读·📖约 8 分钟

数据库系统

mdView 支持三种数据库:SQLite(默认)、MySQLSQL 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 为目标数据库类型
  • 填写对应数据库的连接信息
  • MySQL / SQL Server:需先手动创建数据库(CREATE DATABASE mdview ...
  • 启动应用后,系统会自动建表和填充默认数据
  • 方言抽象层

    SchemaDialect 基类

    文件:core/db/SchemaDialect.php

    定义了所有方言必须实现的方法:

    方法用途 `buildDsn(config)`构造 PDO DSN 连接字符串 `getConnectionOptions()`返回 PDO 连接选项(默认 `ERRMODE_EXCEPTION` + `FETCH_ASSOC`) `initConnection(pdo)`连接建立后的初始化操作(如 SQLite 的 `PRAGMA`) `primaryKeyDef()`自增主键列定义 `textType()`文本类型 `dateTimeType()`日期时间类型 `listColumnsSQL(table)`查询表列名的 SQL `listColumns(pdo, table)`执行列名查询并返回列名数组 `createIndexSQL(...)`创建索引的 SQL `createIndexSafe(pdo, ...)`安全创建索引(已存在则忽略) `upsertSQL(table, cols, conflictKey)`生成 upsert(插入或更新)SQL

    三种方言对比

    特性SQLiteMySQLSQL Server 自增主键`INTEGER PRIMARY KEY AUTOINCREMENT``INT AUTO_INCREMENT PRIMARY KEY``INT IDENTITY(1,1) PRIMARY KEY` 文本类型`TEXT``TEXT``NVARCHAR(MAX)` 日期类型`DATETIME``DATETIME``DATETIME2` Upsert`ON CONFLICT DO UPDATE``ON DUPLICATE KEY UPDATE``MERGE INTO` 列名查询`PRAGMA table_info``INFORMATION_SCHEMA.COLUMNS``INFORMATION_SCHEMA.COLUMNS` 索引创建`CREATE INDEX IF NOT EXISTS``CREATE INDEX IF NOT EXISTS`检查 `sys.indexes` 后创建

    Database 类

    文件:core/Database.php

    单例模式,通过 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()     → 填充默认数据
    

    关键方法

    方法说明 ------------ `getInstance()`获取单例实例 `getPdo()`获取原生 PDO 连接 `getDialect()`获取当前方言实例(模型层 upsert 时使用)

    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()
    );
    

    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);
    

    数据类型映射速查

    SQLiteMySQLSQL Server `INTEGER` / `INT``INT``INT` `INTEGER PRIMARY KEY AUTOINCREMENT``INT AUTO_INCREMENT PRIMARY KEY``INT IDENTITY(1,1) PRIMARY KEY` `TEXT`(无默认值或普通列)`TEXT``NVARCHAR(MAX)` `TEXT`(含 DEFAULT 值)`VARCHAR(N)``NVARCHAR(N)` `TEXT`(主键列)`VARCHAR(N)``NVARCHAR(N)` `DATETIME``DATETIME``DATETIME2` `DATETIME DEFAULT CURRENT_TIMESTAMP`同左`DATETIME2 DEFAULT GETDATE()` `CREATE TABLE IF NOT EXISTS`同左`IF NOT EXISTS (...) CREATE TABLE` `CREATE INDEX IF NOT EXISTS`同左查询 `sys.indexes` 判断

    说明
    - 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,继承 SchemaDialect
  • 实现所有抽象方法
  • Database::createDialect() 中添加 case 分支
  • config.php 中添加对应的配置段
  • 注意事项

  • SQLite 模式下 config.php 不是必需文件,系统会自动回退
  • MySQL / SQL Server 需要手动预先创建数据库
  • SQL Server 需要安装 pdo_sqlsrv PDO 驱动
  • Schema 迁移(migrate)会自动检测已有列,只添加缺失的列
  • 所有字段类型使用 ? PDO 占位符,不受数据库差异影响