资讯专栏INFORMATION COLUMN

Phalcon数据库抽象层

klinson / 3330人阅读

摘要:数据库抽象层是底层组件,由它驱动框架中的模型层。它完全由语言编写,是一个独立的数据库高级抽象层。使用任何数据库适配器,数据都会被自动转义。配合使用,可以在数据库抽象层上提供日志记录功能。语法因数据库而异。并非所有数据库系统都允许修改列或

数据库抽象层(Database Abstraction Layer)

PhalconDbPhalconMvcModel底层组件,由它驱动框架中的模型层。它完全由C语言编写,是一个独立的数据库高级抽象层。

与传统模型相比,该组件允许更底层的数据库操作。

数据库适配器(Database Adapters)

该组件使用适配器来封装特定的数据库操作。Phalcon使用PDO连接数据库,支持下列数据库引擎:

说明
PhalconDbAdapterPdoMysql 世界上最流行的关系型数据库系统(RDBMS),作为服务器运行,支持多用户、多数据库访问
PhalconDbAdapterPdoPostgresql Postgresql是一个强大的开源关系数据库系统,超过15年的发展和通过验证的架构,为其赢得了正确、可靠、数据完整的良好声誉
PhalconDbAdapterPdoSqlite SQLite是一个实现自包含、无服务、零配置的事务型数据库
工厂类(Factory)

使用适配器选项加载PDO:

 "localhost",
    "dbname"   => "blog",
    "port"     => 3306,
    "username" => "sigma",
    "password" => "secret",
    "adapter"  => "mysql",
];

$db = Factory::load($options);
自定义适配器(Implementing your own adapters)

创建自定义数据库适配器或扩展现有适配器,必须实现PhalconDbAdapterInterface接口。

数据库语言(Database Dialects)

phalcon语言封装了每个数据库的具体操作,为适配器提供通用方法和SQL生成器。

说明
PhalconDbDialectMysql MySQL特定语言
PhalconDbDialectPostgresql Postgresql特定语言
PhalconDbDialectSqlite SQLite特定语言
自定义语言(Implementing your own dialects)

创建自定义数据库语言或扩展现有语言,必须实现PhalconDbDialectInterface接口。

连接数据库(Connection to Databases)

建立数据库连接,必须实例化适配器类,它只接收一个包含连接参数的数组。下面例子展示了如何传递必选参数和可选参数来建立数据库连接:

 "127.0.0.1",
    "username" => "mike",
    "password" => "sigma",
    "dbname"   => "test_db",
];

// 可选参数
$config["persistent"] = false;

// 建立连接
$connection = new PhalconDbAdapterPdoMysql($config);
 "localhost",
    "username" => "postgres",
    "password" => "secret1",
    "dbname"   => "template",
];

// 可选参数
$config["schema"] = "public";

// 建立连接
$connection = new PhalconDbAdapterPdoPostgresql($config);
设置额外的PDO选项(Setting up additional PDO options)

在建立连接时,传递options参数设置PDO:

 "localhost",
        "username" => "root",
        "password" => "sigma",
        "dbname"   => "test_db",
        "options"  => [
            PDO::MYSQL_ATTR_INIT_COMMAND => "SET NAMES UTF8",
            PDO::ATTR_CASE               => PDO::CASE_LOWER,
        ],
    ]
);
使用工厂类连接数据库(Connecting using Factory)

使用一个简单的ini文件来配置 / 连接数据库。

[database]
host     = TEST_DB_MYSQL_HOST
username = TEST_DB_MYSQL_USER
password = TEST_DB_MYSQL_PASSWD
dbname   = TEST_DB_MYSQL_NAME
port     = TEST_DB_MYSQL_PORT
charset  = TEST_DB_MYSQL_CHARSET
adapter  = mysql
set("config", $config);

$di->set(
    "db",
    function () {
        return Factory::load($this->config->database);
    }
);

上述代码返回数据库连接实例,这样做的好处是可以在不修改应用代码的情况下改变数据库连接甚至是数据库适配器。

查询记录(Finding Rows)

PhalconDb提供了多种查询方法。这种情况下,SQL必须遵循数据库引擎的特定语法:

query($sql);

// 打印robot的name字段
while ($robot = $result->fetch()) {
    echo $robot["name"];
}

// 获取结果集数组
$robots = $connection->fetchAll($sql);
foreach ($robots as $robot) {
    echo $robot["name"];
}

// 获取结果集中的第一条记录
$robot = $connection->fetchOne($sql);

默认情况下,调用这些方法会返回一个数组(关联+索引)。可以调用PhalconDbResult::setFetchMode()方法改变这种行为,该方法接收一个常量值,定义返回结果集的类型:

常量 说明
PhalconDb::FETCH_NUM 返回索引数组
PhalconDb::FETCH_ASSOC 返回关联数组
PhalconDb::FETCH_BOTH 返回数组(索引+关联)
PhalconDb::FETCH_OBJ 返回对象
query($sql);

$result->setFetchMode(PhalconDb::FETCH_NUM);
while ($robot = $result->fetch()) {
    echo $robot[0];
}

PhalconDb::query()方法返回一个PhalconDbResultPdo实例。该对象封装了与返回结果集相关的所有功能,如遍历、查找特定行、统计总行数等。

query($sql);

// 遍历结果集
while ($robot = $result->fetch()) {
    echo $robot["name"];
}

// 查找第三行
$result->seek(2);
$robot = $result->fetch();

// 计算总行数
echo $result->numRows();
参数绑定(Binding Parameters)

PhalconDb支持参数绑定。使用参数绑定会影响性能,但可以防止SQL注入。
支持字符串和数字占位符,参数绑定可以简单的实现如下:

query(
    $sql,
    [
        "Wall-E",
    ]
);

// 字符串占位符
$sql     = "INSERT INTO `robots`(name, year) VALUES(:name, :year)";
$success = $connection->query(
    $sql,
    [
        "name" => "Astro Boy",
        "year" => 1952,
    ]
);

使用数字占位符时,需要将它们定义为数字值(如1或2),"1"或"2"会被视为字符串而非数字,导致占位符不能被成功替换。使用任何数据库适配器,数据都会被Pdo::Quote()自动转义。该方法会考虑到连接字符集,因此建议在连接选项或服务器配置中定义正确的字符集,错误的字符集会在存储或检索数据时产生不良影响。
此外,可以将参数直接传递给execute() / query()方法,这种情况下的绑定参数会直接传递给PDO:

query(
    $sql,
    [
        1 => "Wall-E",
    ]
);
特定类型占位符(Typed Placeholders)

占位符允许执行参数绑定以避免SQL注入:

 :id:";

$robots = $this->modelsManager->executeQuery(
    $phql,
    [
        "id" => 100,
    ]
);

某些数据库系统在使用占位符时需要执行额外操作,如指定绑定参数的类型:

modelsManager->executeQuery(
    $phql,
    [
        "number" => 10,
    ],
    Column::BIND_PARAM_INT
);

可以在参数中使用类型化的占位符,而不用在executeQuery()方法中指定:

modelsManager->executeQuery(
    $phql,
    [
        "number" => 10,
    ]
);

$phql   = "SELECT * FROM StoreRobots WHERE name <> {name:str}";
$robots = $this->modelsManager->executeQuery(
    $phql,
    [
        "name" => $name,
    ]
);

如果不需要指定绑定参数类型,可以省略:

 {name}";
$robots = $this->modelsManager->executeQuery(
    $phql,
    [
        "name" => $name,
    ]
);

类型化的占位符很强大,我们可以绑定静态数组,而无需将每个参数作为占位符多带带传递:

modelsManager->executeQuery(
    $phql,
    [
        "ids" => [1, 2, 3, 4],
    ]
);

支持下列类型:

绑定类型 绑定类型常量 示例
str Column::BIND_PARAM_STR {name:str}
int Column::BIND_PARAM_INT {number:int}
double Column::BIND_PARAM_DECIMAL {price:double}
bool Column::BIND_PARAM_BOOL {enabled:bool}
blob Column::BIND_PARAM_BLOB {image:blob}
null Column::BIND_PARAM_NULL {exists:null}
array Column::BIND_PARAM_STR数组 {codes:array}
array-str Column::BIND_PARAM_STR数组 {names:array}
array-int Column::BIND_PARAM_INT数组 {flags:array}
绑定参数类型转换(Cast bound parameters values)

默认情况下,绑定参数不会在PHP中转换为指定类型,
如,在LIMIT / OFFSET中给占位符传递一个字符串值就会导致错误:

executeQuery(
    "SELECT * FROM SomeRobots LIMIT {number:int}",
    [
        "number" => $number,
    ]
);

这会导致异常:

Fatal error: Uncaught exception "PDOException" with message "SQLSTATE[42000]:
Syntax error or access violation: 1064 You have an error in your SQL syntax;
check the manual that corresponds to your MySQL server version for the right
syntax to use near ""100"" at line 1" in /Users/scott/demo.php:78

错误原因是"100"是一个字符串。可以先将值转换为整型:

executeQuery(
    "SELECT * FROM SomeRobots LIMIT {number:int}",
    [
        "number" => (int) $number,
    ]
);

要解决这个问题,需要开发者格外注意绑定参数类型及其如何传递。为了简化操作并避免异常,可以指定Phalcon自动转换:

 true]);

以下操作根据指定的绑定类型执行:

绑定类型 操作
Column::BIND_PARAM_STR 将值转换为PHP字符串
Column::BIND_PARAM_INT 将值转换为PHP整型
Column::BIND_PARAM_BOOL 将值转换为PHP布尔值
Column::BIND_PARAM_DECIMAL 将值转换为PHP浮点数

从数据库返回的值在PDO中始终表示为字符串,无论该列值是数字还是布尔值。这种情况是因为某些列类型由于其大小限制而无法用PHP原来类型表示。例如,MySQL中的BIGINT可以存储无法用PHP 32位整型表示的大整数。所以,PDO和ORM默认将所有值作为字符串。可以设置ORM自动将这些值转换为PHP实际类型:

 true]);

通过这种方式,可以使用严格运算符或对变量类型进行假设:

id) {
    echo $robot->name;
}
插入 / 更新 / 删除记录(Inserting / Updating / Deleting Rows)

可以使用原生SQL或类方法来插入、更新、删除记录:

execute($sql);

// 使用占位符
$sql     = "INSERT INTO `robots`(`name`, `year`) VALUES(?, ?)";
$success = $connection->execute(
    $sql,
    [
        "Astro Boy",
        1952,
    ]
);

// 动态生成SQL语句
$success = $connection->insert(
    "robots",
    [
        "Astro Boy",
        1952,
    ],
    [
        "name",
        "year",
    ]
);

// 动态生成SQL语句(另一种语法)
$success = $connection->insertAsDict(
    "robots",
    [
        "name" => "Astro Boy",
        "year" => 1952,
    ]
);

// 使用原生SQL更新数据
$sql     = "UPDATE `robots` SET `name` = "Astro Boy" WHERE `id` = 101";
$success = $connection->execute($sql);

// 使用占位符
$sql     = "UPDATE `robots` SET `name` = ? WHERE `id` = ?";
$success = $connection->execute(
    $sql,
    [
        "Astro Boy",
        101,
    ]
);

// 动态生成SQL语句
$success = $connection->update(
    "robots",
    [
        "name",
    ],
    [
        "New Astro Boy",
    ],
    "id = 101" // 注意,这种情况下值不会被自动转义
);

// 条件中数据的转义
$success = $connection->update(
    "robots",
    [
        "name",
    ],
    [
        "New Astro Boy",
    ],
    [
        "conditions" => "id = ?",
        "bind"       => [101],
        "bindTypes"  => [PDO::PARAM_INT], // 可选参数
    ]
);
$success = $connection->updateAsDict(
    "robots",
    [
        "name" => "New Astro Boy",
    ],
    [
        "conditions" => "id = ?",
        "bind"       => [101],
        "bindTypes"  => [PDO::PARAM_INT], // 可选参数
    ]
);

// 使用原生SQL删除记录
$sql     = "DELETE `robots` WHERE `id` = 101";
$success = $connection->execute($sql);

// 使用占位符
$sql     = "DELETE `robots` WHERE `id` = ?";
$success = $connection->execute($sql, [101]);

// 动态生成SQL语句
$success = $connection->delete(
    "robots",
    "id = ?",
    [
        101,
    ]
);
事务和嵌套事务(Transactions and Nested Transactions)

PDO支持事务处理,在事务内部执行数据库操作通常可以提高数据库的性能:

begin();

    // 执行SQL语句
    $connection->execute("DELETE `robots` WHERE `id` = 101");
    $connection->execute("DELETE `robots` WHERE `id` = 102");
    $connection->execute("DELETE `robots` WHERE `id` = 103");

    // 如果一切顺利,提交事务
    $connection->commit();
} catch (Exception $e) {
    // 发生异常,回滚事务
    $connection->rollback();
}

除了标准事务,PhalconDb内置了嵌套事务(如果数据库支持)。当再次调用begin()方法时,会创建一个嵌套事务:

begin();

    // 执行SQL语句
    $connection->execute("DELETE `robots` WHERE `id` = 101");

    try {
        // 开始嵌套事务
        $connection->begin();

        // 嵌套事务中执行SQL语句
        $connection->execute("DELETE `robots` WHERE `id` = 102");
        $connection->execute("DELETE `robots` WHERE `id` = 103");

        // 创建保存点
        $connection->commit();
    } catch (Exception $e) {
        // 发生异常,回滚嵌套事务
        $connection->rollback();
    }

    // 继续执行更多SQL语句
    $connection->execute("DELETE `robots` WHERE `id` = 104");

    // 如果一切顺利,提交事务
    $connection->commit();
} catch (Exception $e) {
    // 发生异常,回滚事务
    $connection->rollback();
}
数据库事件(Database Events)

PhalconDb能够将事件发送给EventManager(如果存在),某些事件返回false时,可能会终止操作。支持以下事件:

事件名称 触发时机 是否会终止操作
afterConnect 成功连接到数据库后
beforeQuery 执行SQL语句前
afterQuery 执行SQL语句后
beforeDisconnect 关闭临时数据库连接前
beginTransaction 事务开启前
rollbackTransaction 事务回滚前
commitTransaction 事务提交前

将EventsManager绑定到数据库连接很简单,PhalconDb将触发db类型事件:

attch("db", $dbListener);

$connection = new Connection(
    [
        "host"     => "localhost",
        "username" => "root",
        "password" => "secret",
        "dbname"   => "invo",
    ]
);

// 将eventsManager分配给数据库适配器实例
$connection->setEventsManager($eventsManager);

数据库事件中,终止SQL操作非常有用。例如,想在SQL执行前实现注入检查:

attch(
    "db:beforeQuery",
    function (Event $event, $connection) {
        $sql = $connection->getSQLStatement();

        // 检查SQL中是否有恶意关键字
        if (preg_match("/DROP|ALTER/i", $sql)) {
            // 不允许DROP / ALTERT操作
            return false;
        }

        return true;
    }
)
分析SQL语句(Profiling SQL Statements)

PhalconDb内置了性能分析组件PhalconDbProfiler,用于分析数据库性能,以便诊断问题,发现瓶颈。使用PhalconDbProfiler进行数据库分析相当容易:

attch(
    "db",
    function (Event $event, $connection) use ($profiler) {
        if ($event->getType() === "beforeQuery") {
            $sql = $connection->getSQLStatement();

            // 开始分析
            $profiler->startProfile($sql);
        }

        if ($event->getType() === "afterQuery") {
            // 停止分析
            $profiler->stopProfile();
        }
    }
);

// 将事件管理器分配给数据库连接
$connection->setEventsManager($eventsManager);

$sql = "SELECT buyer_name, quantity, product_name FROM buyers LEFT JOIN products ON buyers.pid = products.id";

// 执行SQL语句
$connection->query($sql);

// 获取最后一个分析结果
$profile = $profiler->getLastProfile();

echo "SQL Statement: ", $profile->getSQLStatement(), "
";
echo "Start Time: ", $profile->getInitialTime(), "
";
echo "Final Time: ", $profile->getFinalTime(), "
";
echo "Total Elapsed Time: ", $profile->getTotalElapsedSeconds(), "
";

还可以基于PhalconDbProfiler创建自己的分析器,以实时统计发送到数据库的SQL语句:

getSQLStatement();
    }

    // SQL语句发送到数据库服务器之后执行
    public function afterEndProfile(Item $profile)
    {
        echo $profile->getTotalElapsedSeconds();
    }
}

// 创建事件管理器
$eventsManager = new EventsManager();

// 创建事件监听器
$dbProfiler = new DbProfiler();

// 设置监听器监听所有数据库事件
$eventsManager->attch("db", $dbProfiler);
记录SQL语句(Logging SQL Statements)

使用诸如PhalconDb这样的高级抽象组件来访问数据库时,很难获知哪些语句被发送到了数据库。PhalconLogger配合PhalconDb使用,可以在数据库抽象层上提供日志记录功能。

attch(
    "db:beforeQuery",
    function (Event $event, $connection) use ($logger) {
        $sql = $connection->getSQLStatement();

        $logger->log($sql, Logger::INFO);
    }
);

// 将eventsManager分配给数据库适配器实例
$connection->setEventsManager($eventsManager);

// 执行SQL语句
$connection->insert(
    "products",
    [
        "Hot pepper",
        3.50,
    ],
    [
        "name",
        "price",
    ]
);

如上所述,文件app/logs/db.log将包含下列内容:

[Sun, 29 Apr 12 22:35:26 -0500][DEBUG][Resource Id #77] INSERT INTO products
(name, price) VALUES ("Hot pepper", 3.50)
自定义记录器(Implementing your own Logger)

可以自定义记录器以记录数据库操作,通过创建一个实现了log()方法的类,该方法接受一个字符串作为第一个参数。可以将记录器对象传递给PhalconDb::setLogger(),这样在执行任何SQL语句时将调用log()方法进行记录。

获取表 / 视图详情(Describing Tables / Views)

PhalconDb提供了获取表、视图详情的方法:

listTables("test_db");

// 表"robots"是否存在于当前库中
$exists = $connection->tableExists("robots");

// 获取"robots"表字段名称、类型、特性
$fields = $connection->describeColumns("robots");
foreach ($fields as $field) {
    echo "Column Type: ", $field["Type"];
}

// 获取"robots"表索引
$indexes = $connection->describeIndexes("robots");
foreach ($indexes as $index) {
    print_r(
        $index->getColumns()
    );
}

// 获取"robots"表外键
$references = $connection->describeReferences("robots");
foreach ($references as $reference) {
    // 打印引用列
    print_r(
        $reference->getReferenceColumns()
    );
}

表详情和MySQL的describe命令返回的信息相似,包含如下信息:

Field Type Key Null
字段名称 字段类型 是否主键或索引列 是否允许为空

对于被支持的数据库系统,同样实现了获取视图详情的方法:

listViews("test_db");

// 视图"robots"是否存在于当前库中
$exists = $connection->viewExists("robots");
创建、修改、删除表(Creating / Alerting / Dropping Tables)

不同的数据库系统(MySQL,Postgresql等)通过CREATE、ALTER、DROP命令提供了用于创建、修改、删除数据表的功能。SQL语法因数据库而异。PhalconDb为编辑表提供了统一接口,无需区分不同数据库系统的SQL语法。

创建表(Creating Tables)

下面例子展示如何创建表:

createTable(
    "robots",
    null,
    [
        "columns" => [
            new Column(
                "id",
                [
                    "type"          => Column::TYPE_INTEGER,
                    "size"          => 10,
                    "notNull"       => true,
                    "autoIncrement" => true,
                    "primary"       => true,
                ]
            ),
            new Column(
                "name",
                [
                    "type"    => Column::TYPE_VARCHAR,
                    "size"    => 70,
                    "notNull" => true,
                ]
            ),
            new Column(
                "year",
                [
                    "type"    => Column::TYPE_INTEGER,
                    "size"    => 11,
                    "notNull" => true,
                ]
            ),
        ],
    ]
);

PhalconDb::createTable()接收一个描述数据表的关联数组,用PhalconDbColumn类创建字段,下表列出了定义字段的选项:

选项 说明 是否可选
type 字段类型,必须是PhalconDbColumn常量
primary 是否主键
size VARCHARINTEGER类型的字段定义字段长度
scale DEMICALNUMBER类型字段定义数据精度
unsigned INTEGER类型字段定义是否有符号,该选项不适用于其他类型字段
notNull 字段是否非空
default 默认值
autoIncrement 是否自增
bind BIND_TYPE_*常量定义字段在保存前如何绑定数据
first 把字段设置为表的第一个字段
after 设置字段放在指定字段之后

PhalconDb支持下列字段类型:

PhalconDbColumn::TYPE_INTEGER

PhalconDbColumn::TYPE_DATE

PhalconDbColumn::TYPE_VARCHAR

PhalconDbColumn::TYPE_DECIMAL

PhalconDbColumn::TYPE_DATETIME

PhalconDbColumn::TYPE_CHAR

PhalconDbColumn::TYPE_TEXT

传入PhalconDb::createTable()方法的关联数组可能包含下列索引:

索引 说明 是否可选
columns PhalconDbColumn定义的字段组成的数组
indexes PhalconDbIndex定义的表索引组成的数组
references PhalconDbReference定义的表引用(外键)组成的数组
options 包含表创建选项的数组,这些选项通常与数据库迁移相关
编辑表(Alerting Tables)

随着应用程序越来越庞杂,可能需要调整数据库,作为重构或添加新功能的一部分。并非所有数据库系统都允许修改列或者新增列,PhalconDb也受到这些限制:

addColumn(
    "robots",
    null,
    new Column(
        "robot_type",
        [
            "type"    => Column::TYPE_VARCHAR,
            "size"    => 32,
            "notNull" => true,
            "after"   => "name",
        ]
    )
);

// 编辑列
$connection->modifyColumn(
    "robots",
    null,
    new Column(
        "name",
        [
            "type"    => Column::TYPE_VARCHAR,
            "size"    => 40,
            "notNull" => true,
        ]
    )
);

// 删除"name"列
$connection->dropColumn(
    "robots",
    null,
    "name"
);
删除表(Dropping Tables)

删除表示例:

dropTable("robots");

// 从"machines"库中删除"robots"表
$connection->dropTable("robots", "machines");

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/28531.html

相关文章

  • Phalcon模型

    摘要:使用模型模型表示应用程序信息数据以及这些数据的处理规则,主要用于管理与对应数据表的交互规则。应用中,是所有模型的基类。创建模型模型需继承类,以大驼峰格式命名。方法在请求期间只调用一次,目的是为该模型的所有实例执行初始化操作。 使用模型(Working with Models) 模型表示应用程序信息(数据)以及这些数据的处理规则,主要用于管理与对应数据表的交互规则。大多数情况下,数据库中...

    chanjarster 评论0 收藏0
  • Phalcon入门教程之模型

    摘要:原文发表于入门教程之模型提供了四种方式操作数据库模型数据库抽象层以及原生。创建模型模型类的命名必须符合驼峰命名法,而且须继承自类文件路径继承自类。 原文发表于:Phalcon入门教程之模型 Phalcon 提供了四种方式操作Mysql数据库:模型、PHQL、数据库抽象层以及原生SQL。不论何种方式,首先都需要在DI中注册 db 服务才能正常使用: DI注册db服务 // 文件路径:...

    FreeZinG 评论0 收藏0
  • Phalcon查询语言

    摘要:查询语言查询语言,简称或,是一种面向对象的高级语言,允许用标准化的编写。该对象的每个成员都是一个包含所查询字段的标准对象。 Phalcon查询语言(Phalcon Query Language) Phalcon查询语言,简称PhalconQL或PHQL,是一种面向对象的高级SQL语言,允许用标准化的SQL编写。PHQL实现了把操作语句解析为RDBMS目标语言的解析器(C语言编写)。 为...

    Moxmi 评论0 收藏0
  • PHP框架Phalcon 之 ACL

    摘要:一般至少要在执行路由前要判断用户是否具有权限一般在中,所以应该在它之前获得填充。以下代码可参考这里的方法就是重点。参考这里把对象保存在中。 showImg(https://segmentfault.com/img/bVkdih); 使用如下图解释这个组件: showImg(https://segmentfault.com/img/bVkdii); 实际最终真正要使用的是access_l...

    mikyou 评论0 收藏0
  • phalcon简易指南

    摘要:帮助你开始使用的简易指南。第一种方式参考第二种方式参考使用参考简单粗暴的理解是把下的对应成数据库的表,类属性对应表字段。 帮助你开始使用 phalcon 的简易指南。 简介 Phalcon 2将于2015年4月17日发布,这个版本大约85%的代码是基于 Zephir 语言重写的。Zephir是开源的,使用类似PHP语法的语言,生成C语言代码,并编译成PHP扩展。这提高了PHP扩展的开发...

    whataa 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<