ZendFramework 入门教程

1、Zend Framework简介

1. 什么是ZendFramework

Zend Framework(ZF or ZFW)是PHP的母公司Zend公司开发的一套PHP开发框架技术,它提供了一个优秀的、简单的综合开发环境,提供了不少可用的解决方案,能够用来创建一个稳定的、可升级的的Web应用。javascript

所谓框架,是整个或者部分系统的可重用设计,它首先要提供一个可复用的应用参考架构,阐明整个设计、组件之间的依赖关系、责任分配和控制流程,也包含一些设计规范等等。它提供了对一些通用问题的解决方案。php

另外Zend Framework采用常见的MVC模型(在后续具体介绍),这样能够比较方便的达到关注点分离的目的,能够比较方便的创建基于MVC架构的Web应用(典型的MVC Web架构还有Struct等等,它们原理都很相似)(PHP的相似框架还有不少如:yaf)css

Zend的官网: http://www.zend.com/en/html

2. 什么是LAMP

LAMP是Linux+Apache+Mysql+Perl/PHP/Python的缩写,它们是一组常常用来搭建动态网站或者服务器的开源软件,自己都是各自独立的程序,可是由于常被放在一块儿使用,拥有了愈来愈高的兼容度,共同组成了一个强大的Web应用程序平台,因为都是开源软件,除了无偿使用的诱惑,还有能够修改源码、本身进行控制等优势,LAMP是大多数网站开发者和不少大公司(如:Facebook和Baidu)的不二选择。从网站的流量上来讲,70%以上的访问流量是LAMP来提供的,可见LAMP是最强大的网站解决方案.(其它相似的方案如MS的.NET框架和Oracle的J2EE框架,三者一样强大)前端

为何要说LAMP呢,由于这些软件的组合强大到只要提到一个就必需要提到另外三个的地步,而在实验室网站的建设中,咱们使用PHP,咱们采起的固然也是LAMP的框架java

3. Zend Framework的安装

XAMPP:Apache Friends

说到安装,LAMP的安装是十分麻烦的,由于这些软件都是相互独立的软件,而开源软件的特色是安装须要进行配置文件的修改,这些软件又相互依赖,因此配置起来很麻烦,常常出现问题,固然要是想了解LAMP并进行后期的优化和个性化配置的话,了解每一个软件的配置方法是必须的,可是若是在起步的时候碰到很是多的困难总会影响你们的积极性,而XAMPP的出现解决了这个问题。XAMPP是一个易于安装的LAMP框架的集成发行版本,它包含了MySQL、PHP、Perl、Apache,网站在http://www.apachefriends.org/zh_cn/xampp.html,它支持Linux、Windows和Mac多种平台,其安装也是很是简单的,和正常软件同样,只要进行下载,运行软件便可。node

安装完成以后,能够启动XAMPP Control Panel查看上述软件的运行状况,还能够在浏览器里面输入http://localhost 对XAMPP的集成软件进行管理。(通常Apache绑定到80端口,能够修改,修改的话要使用:XX的方法进行访问.)mysql

Zend安装和使用

ZendFramework的安装只须要到Zend Framework的官方网站http://framework.zend.com/download下载Zend Framework的程序安装包解压到某个指定目录下,而后再php.ini(PHP的配置文件)中的include_path加入Zend的解压目录便可。jquery

另外须要注意的是,默认的Zend Framework使用MVC机制,它采用rewrite的方式进行跳转,这就须要在apache的配置文件中(通常是httpd.conf)修改加入容许rewrite的选项,须要的步骤是:sql

1. 找到LoadModule rewrite_modulemodules/mod_rewrite.so将其前面的#去掉

2. 在项目所在的目录下<Directory “projectPath”>中修改AllowOverride的值为All,Order allow,deny / Allow from all

3. 在项目所在根目录下创建.htaccess文件,内容为

RewriteEngine on #重写引擎打开

RewriteRule!\.(js|ico|gif|jpg|png|css)$ index.php

#制定除js,ico,gif,jpg,png,css之外的文件全都被重置到index.php,index.php为项目的首页(实际上是前端转发控制页)

简单的Zend Framework安装测试方法:

建立文件test.php内容:

<?php

require_once(‘Zend/Date.php’);

$date=new Zend_Date();

echo $date

?>

若是能正常输出则说明Zend安装大致正常。

另外,若是想要在View文件中使用<?=someVariable?>代替<?php echo $value ?>的功能,须要在php.ini中开启short opentag, short_open_tag = On才可,不然<?=?>的表达式不能被正常解析。

2、MVC框架

1. MVC框架是什么

MVC(Model-View-Controller)是一种集成了不少设计模式(Design Pattern)的一种设计模式,它强制的将程序的输入、处理、输出分开,它把程序分为三个核心部件:Model,View和Controller,其中

Model:负责数据的处理,包含业务逻辑。

View:负责数据的展示,并获取输入。

Controller:负责从View处接收输入,并操做Model来完成用户需求,而后调用View返回数据给用户。

2. 典型的MVC框架例子

在这里举两个MVC的例子,一个是在传统Desktop程序,另外一个以Zend Framework为例,详细介绍ZendFramework的MVC。

2.1传统Desktop程序:文本编辑器

文本编辑器你们应该都用过,在这里使用一个在《深刻浅出MFC》中侯捷先生使用的例子,其所要实现的功能没必要赘述,有一个文本框负责展现文字,用户能够对文字进行修改。

其中的:

Model,负责调用操做系统底层文件操做API,完成的功能有

a) 读取文件内容

b) 修改文件内容:包括编辑和删除内容

c) 建立文件、删除文件、

View,负责展现当前文件的内容给用户,而且负责提供UI界面,用以操做文本,完成的功能有:

a) 展现文本当前内容

b) 获取用户可能的输入:文本选择,文本复制,文本粘贴,文本编辑,文件的打开,删除和新建等等。

Controller,负责响应从View层传递来的用户请求,调用相应的Model操做,来执行用户的需求,完成的功能有:

a) 响应View请求,将其转换成对应的Model方法调用

b) 完成操做后,将返回结果返回给指定View,向用户展现结果

(注:在文本编辑器的功能中,有这样一种状况,有多个文本窗口,展现文件内容,当有多个窗口同时对一个文件进行操做,这样,就有了共同资源的修改问题,而在一个文本窗口修改了文本以后,其它的文本编辑器也应该显示修改过的值,这就须要每一个文本窗口对文件状态有实时的了解,该如何实现?使用观察者模式.)

3. Zend中的MVC框架

在这里,咱们将简单介绍Zend Framework的MVC组件,并大体给出一个Zend Framework程序的简单实现。ZendFrameworkd的MVC框架与传统的MVC程序框架思想相似,可是因为应用于互联网环境中,又有了少量不一样。

因为使用MVC框架,咱们须要对项目的目录结构给出规范,典型的目录结构以下

app/

----controllers/

----models/

----views/

----------scripts/

其中app是根目录下的子目录,保存MVC代码,controllers中保存全部的controllers,models目录保存全部models代码,views/scripts中保存全部的view代码。

3.1 Zend_Controller

Zend_Controller是Zend Framework中的Controller部分,其中的Zend_Controller_Front类实现了前端控制器设计模式,

3.1.1前端控制器

前端控制器设计模式:因为互联网环境的全部请求都有httpurl请求从浏览器发送给服务器,Web应用的输入对于服务器来讲就是请求,为了实现从请求->对应Controller的映射,咱们将全部请求都首先发送到前端控制器front controller,由前端控制器进行配置,选择合适的Controller进行分发(dispatch)请求,再由具体Controller进行请求的响应处理。(前端控制器设计模式在Web程序中很常见,Structs也采用这种模式)

通常的前端控制器都在入口文件index.php中定义(根目录下),声明代码为:

// 初始化frontcontroller实例

$front=Zend_Controller_Front::getInstance();

// 开启分发过程当中抛出异常的能力,默认异常引发以后会被放置在想要对象中

$front->throwExceptions(true);

// 设置默认的处理Controller名称

$front->setDefaultControllerName('home');

// 设置controllers目录

$front->setControllerDirectory($dir.'\app\controllers');

 

在全部的配置都已完毕之时,前端控制器须要负责整个程序(像是Win32程序中的主程序)的请求转发,(相似处理消息循环),并处理分发请求:如

try {

// 开始运行

$front->dispatch();

 

} catch (Exception $e) {

// 处理异常

echo $e;//include_once'exception.php';

}

FrontController的dispatch方法能够实现Controller的路由、分发、响应等工做,分发过程由三个不一样的事件组成:

² 路由(Routing),使用路由器router,只执行一次,在调用dispatch()方法时利用请求对象中的值进行。

² 分发(Dispatching),使用分发器dispatcher,发生在循环中

² 响应(Response),完成处理后前端控制器返回响应对象

3.1.2动做控制器

Zend_Controller:每个具体的Controller都应该是继承于类Zend_Controller_Action,是一个动做控制器类,此类有最基本的类Controller方法,用来处理相关的View和Model之间的操做,每一个具体的Controller都应该继承这个类。

动做控制器:每一个Controller中的publicfunction xxxAction()方法都是一个控制器动做,其中定义了Controller面对不一样的请求时应该进行的动做。好比在请求http://localhost/home/index这个url的时候,请求就会转发到home Controller的indexAction动做中,indexAction会定义对这个请求的处理方法。

l 操做request\response对象、获取参数:

在动做控制器函数(xxxAction)中,能够经过API访问一些经常使用的数据,如:

² request对象:由getRequest()方式取得,返回一个Zend_Controller_Request_Abstract实例

² response对象:由getResponse()取得,返回一个Zend_Controller_Response_Abstract实例,可用此对象上的方法来修改response对象

² request参数:由post或者get传递过来的参数,能够用_getParam($key)方法或者_getAllParams()获取,也能够由_setParam方法来设置request参数(通常用于转发请求到其余动做)

l 操做视图:

² 视图由Zend_View对象来表示,在Zend_Controll_Action中能够经过initView()或render()方法来初始化对应的Zend_View对象,默认状况下假定视图存在views/scripts/子目录中,指定view脚本时基准目录为views/scripts/

(通常是在Zend_Controller_Action的init方法中使用initView,在其余xxxAction中使用render)

² render()用于解析视图,声明为render(string$action=null,string $name=null,bool $noController=false),若是不传递参数,默认的解析脚本是在views/scripts/[controller名]/[action名].$viewSuffix的值

² 注:控制器和动做名称中包含”_”,”.”,”-”的话,这些分隔符会被格式化成”-”

l 转向方法:

² _forward($action…)方法,用于执行另一个动做

² _redirect($url…)方法,用于重定向到另一个地方

3.1.3动做助手

动做助手(Zend_Controller_Action_Helper)能够向Zend_Controller_Action类的动做控制器加入功能(runtimeand/or on-demand functionality).

助手经纪人存在Zend_Controller_Action类中$_helper成员中,用来获取和调用助手。是一个Zend_Controller_Action_HelperBroker类的对象,其支持addHelper方法向经纪人中加入助手(也能够用addPrefix和addPath方法加入)。至关于Action的助手管家。

内建的动做助手

l FlashMessenger:容许用户传递可能须要在下个请求看到的消息,使用Zend_Session_Namespace来存储消息(用于内部的消息传递)

l Redirector: 转向器,能够帮助程序重定向到新的URL

l ViewRenderer:视图渲染助手,能够帮助咱们不在controller中建立view实例,view对象自动在controller注册,能够根据当前模块自动设置视图脚本、助手、过滤器路径,指派当前的模块名为助手和过滤器的类名前缀,能够自动渲染view脚本,咱们就能够不写initView和render了。

使用方法,

// View Render helper

$view=new Zend_View(array('encoding'=>'UTF-8'));

// 建立view render

$view_render=new Zend_Controller_Action_Helper_ViewRenderer($view);

// 设置view脚本的后缀,这里使用.php

$view_render->setViewSuffix('php');

// 将view renderhelper加入动做助手中,这样咱们就可借助渲染助手自动渲染后缀为.php的view脚本啦,渲染的寻找规则和上述默认initView的规则一致.

Zend_Controller_Action_HelperBroker::addHelper($view_render);

3.2 Zend_View

Zend_View的使用方法:在Controller中建立一个Zend_View实例(也能够由View Render Helper帮忙建立,咱们就不用本身写了)。而后将view中须要的变量赋给它就能够了,而后使用render渲染view便可。

通常代码为:

<?php

$view=newZend_View()

$view->a=’’

$view->c=’’//或者用view的assign(),支持关联数组赋值

echo$view->render(‘someView.php’)

?>

咱们采用的方式为在view脚本中使用html与php相混合的形式,要用到的变量由controller传递给它,$this->view->xxx;在脚本中使用$this->xxx的方法访问。

3.3 Zend_Model

在Model部分通常是完成对数据的访问,管理以及实现业务逻辑,通常model的存储目录在和Controllers同根目录的models文件夹中,Model通常就是之间的PHP类就能够了。而通常咱们在构建Web应用中数据都存在数据库里,在这里就简单讨论下Zend中对数据库的支持吧。

Zend_Db组件是Zend Framework中的数据库支持部分,由Zend_Db_Adapter、Zend_Db_Statement、Zend_Db_Profiler、Zend_Db_Select、Zend_Db_Table、Zend_Db_Table_Row以及Zend_Db_Table_Rowset等组成

Zend_Db_Adapter是Zend Framework的数据库抽象层API,是基于PDO的,能够支持多种数据库。Adapter的配置方式为

// 链接mysql数据库

$db=Zend_Db::factory('PDO_MYSQL', $config);

$config中存储一些链接数据库的配置信息,如地址,端口,用户名,密码,使用的数据库名等。在链接以后就能够直接使用$db->query()的方法查询数据库了。

Zend_Db_Adapter的支持的操做:

l query($sql,$bind=array()),查询数据库,$bind为须要绑定的数字

l queryInto($text,$value,$type=null)实现对SQL的无害化处理

l insert($table,array $bind),插入数据,$table为代表,$bind为表的字段与插入数据直接的绑定数组。

l lastInserId($tableName=null,$primaryKey=null),返回刚刚插入数据的ID

l fetchRow($sql,$bind=array()),用于查询SQL的返回结果,返回的结果能够用foreach($resultas $key=>$value)的形式遍历。

l delete($table,$where=’’)在数据库删除表$table记录

l update($table,array $bind,$where=’’)用于在表$table上根据$where的限制条件改变$bind数组相关的键值内容。

由上述的Zend_Db_Adapter就能够大致上的完成数据库的操做,咱们的实验室网站也基本就用了Zend_Db_Adapter的相关内容,而其余的Zend_Db控件则提供了各类更强大的功能,你们能够本身发掘,这里再也不赘述

3、Zend经常使用控件

上面说了这么多MVC什么的,下面经过给出在登陆过程当中的用到的ZendFramework的具体实例,说明Zend Framework的一些简单的组件用法。

1. 从登陆提及

一个简单的登陆过程咱们都再了解不过了,首先咱们须要给出一个输入用户名密码的Form,而后用户输入完用户名密码以后,点击submit提交,咱们的程序将用户输入与后台数据库中的数据进行比较,查看是否身份验证经过,验证经过则保存一份Session便于用户访问,不然则提示用户用户名密码输入错误,从新输入。复杂的登陆控制还包括验证码等内容,咱们在此处不讨论。

注:关于登陆有不少内容可谈,好比单点登陆又是什么?你们能够本身查查看看

 

下面给出登陆控制Controller的Init方法和Login动做:相关的内容在后面补齐:

请求的URL应该相似为:http://localhost/member/login;代表form被说起到memberController的loginAction中进行处理。

首先给出Controller的初始化函数init,通常在这初始化各类属性

function init()

{

// 取得数据库接口,事先存储在对象注册表Zend_Registry中

$this->db=Zend_Registry::get('database');

// 设置view的action和controller属性

$this->view->action=$this->_getParam('action');

$this->view->controller=$this->_getParam('controller');

 

// 默认载入的js,方便在view中使用

$this->view->javascript=array('jquery.inline.js', 'paper.js');

 

// 取得登陆状态,状态存储在Zend_Session中

$session=new Zend_Session_Namespace();

$login=$session->login;

 

if (isset($login)) {

// 给view中相关变量赋值

$this->view->aid=$login['AId'];

$this->view->priv=$login['Priv'];

$this->view->name=$login['Name'];

}

else {

$this->view->aid=$this->view->priv=$this->view->name=0;

}

// 默认标题

$this->view->title='Web首页 ';

}

而后给出login动做处理器

function loginAction()

{

$url='/home/index';

//从request参数中获取用户输入的用户名和密码

$user=$this->_getParam('User');

$pass=md5($this->_getParam('Pass'));

//启动Session

$session=new Zend_Session_Namespace();

try{

$acc=member::login($this->db, $user, $pass); // 调用model查询指定表中有无$user和$pass知足的条目

 

if (is_array($acc)) {

// $acc是数组,说明有知足条件的条目,验证成功,接下来将获得的信息放到$session中里,方便其余模块使用

$session->login= array

('AId'=>$acc['AId'],

'Name'=>$acc['Name'],

'Priv'=>$acc['Priv'],

'User'=>$acc['User']);

}

catch(Zend_Exception $e){

}

// 使用home/index.php页面进行渲染

$this->renderScript('home/index.php');

}

 

2. Zend_Session

在登陆过程成功以后,咱们不可避免的须要保存用户的Session信息,而这个Session信息在Zend Framework中由控件Zend_Session实现。(即由Zend_Session管理PHP中$_SESSION的内容)

咱们知道在PHP中要使用Session首先要开启Session,而咱们使用Zend_Session::newZend Session_Namespace()方法则会默认开启Zend_Session::start()方法,而设置Zend_Session::setOptions(array(‘strict’=>true)); 能够阻止在new Zend_Session_Namespace()时调用Zend_Session::Start()方法。

(通常咱们在使用Session前都会开启Zend_Session::start();)

 

因此咱们使用Zend_Session的通常使用以下两种:

l Zend_Session::setOptions(array(‘strict’=>true));

Zend_Session::Start();

$mySpace=newZend_Session_Namespace(‘MySpace’);

l 或者须要时直接使用

$mySpace=newZend_Session_Namespace(‘MySpace’);

上例中采用第二种方式(其实上面的$mySpace其实就被保存在$_SESSION['MySpace']中)。

3. Zend_Registry

Register是一个对象注册表,能够将任何对象存储在对象注册表中,这样就能够在任何地方随时随地调用,能够说就是一个全局变量管理器。

能够用

$reg=Zend_Registry::getInstance();

$reg->set('database', $db);

来得到一个默认的Zend_Registry注册表变量(本例中的数据库对象),并在其上调用set($key,$value)方法设置全局变量的值,此后在任何地方能够经过Zend_Registry::isRegistered($key)检查是否有对象存在,能够用Zend_Registry::get($key)得到默认的注册表中的对象内容,还能够经过Zend_Registry::_unsetInstance($key)来删除对象注册表内的内容。

 

Zend_Registry和Zend_Session的区别(翻译自网上):

Zend_Registry和Zend_Session的主要区别是在它们的做用域上,Zend_Registry的做用域是对当前的请求(current request)有效,也就是Zend_Register的数据不能被不一样页面所共享,可是若是咱们在index.php(前端转发控制的页面)定义Zend_Register数据,那么在全部的controllers/actions中均可以使用Zend_Register的,由于它们刚刚被重定向到index.php后才传到这些具体的action中,因此这些数据是能够用的。而通常Zend_Registry用于存储配置,共享变量等信息。在每次请求不一样的页面的时候Zend_Register的数据都会被清除。

而Zend_Session使用的是PHP的Session,能够被任何页面共享,它是真的有Session的做用域的(即关闭浏览器以前、或Session过时以前都有效)

4. Zend_Loader

熟悉PHP的人都知道,在PHP中,如何加载Load(include)一个类是一个很麻烦的话题,由此,PHP5中提供了__autoload的机制,方便类文件的自动加载。而Zend框架中也提供了一个能够实现对文件和类进行动态加载的功能Zend_Loader。

提供Zend_Loader的加载功能可用的方法:

l 加载文件

Zend_Loader::loadFile($filename,$dirs=null,$once=false);

$filename是要加载的文件名,$dirs为文件所在的目录,若是为空的话程序会自动到PHP的include_path中找,$once指定是否为只加载一次,为true加载一次,不然重复加载。若是loadFile加载文件失败,此方法会抛出Zend_Exception异常。

l 加载PHP类

Zend_Loader::loadClass($class,$dirs)还能够支持对PHP类的加载,其中$class为类名,$dirs为包含类的所在路径及文件名,另外,此方法会根据下划线对应到目录的文件,Zend_Controller_Action会指向Zend/Controller/Action.php文件。若是$dirs为一个字符串或数组,则方法会顺序查找相应目录,加载第一个匹配的文件,若是没找到的话还会在include_path中查找。

5. Zend_Acl

Zend_Acl组件给出了实现一个完整的访问控制的解决方案,Zend_Acl中定义了两个重要的概念资源和角色,而分别和其对应的两个Zend控件是Zend_Acl_Role和Zend_Acl_Resource。

Zend_Acl_Resource:在Zend_Acl中,任何文件均可以被当作资源对待,Zend_Acl还提供了一个资源的树结构,能够自由添加多个Resources。Zend_Acl也支持基于Resources的create,read,update和delete权限等待。资源能够经过实现Zend_Acl_Resource_Interface来实现,而Zend_Acl_Resource是基本的一个Resource实现,也能够进行扩展实现资源

Zend_Acl_Role: 在Zend_Acl中,角色就是全部进行访问的对象的统称,Role之间能够有继承关系,能够经过Zend_Acl_Role_Interface接口开发Roles,一样Zend_Acl_Role是一个基本的Role实现,能够用来继承扩展

建立ACL的方法

$acl=newZend_Acl();

$acl->addRole($roles,$parentnode);//加入角色

$acl->allow($roles,$resources,$privilege,$assert);//指定用户组$roles在$resources上的权限,$resources使用null的时候表明全部资源,$privilege能够为create,edit,delete等等,而$assert用于指定有特定约束的控制,如在必定时间段内不容许等等。

$acl->deny($roles,$resources,$privilege,$assert);//为指定用户组$roles在$resources上添加拒绝权限

 

定义了Acl以后,咱们就能够用Acl的isAllowed方法来检查某个用户在某个资源上是否有权限了如

$acl->isAllowed($roles,$resources,$privilege);

6. Zend_Auth

访问认证适配器,用于给出一个对用户访问进行认证的方法,Zend_Auth依靠特定的认证服务来进行认证(如RDBMS)

Zend_Auth提供了不少种的系统默认认证访问支持,如今在这里只简单说明数据库认证方式。

Zend_Auth_Adapter_DbTable是Zend_Auth提供的数据库访问方案,它的默认构造函数的参数须要有:

l $zendDb,一个Zend_Db_Adapter的适配器实例

l $tableName,数据库的表名称

l $identityColumn:指定表中的记录列

l $credentialColum:指定表中的凭记列(密码列)

l $credentialTreatment:指定对表中的凭记列进行的加密操做

1. 对象的初始化方式也能够以下:(以set的方式替代构造函数参数)

$dbAdapter = Zend_Db_Table::getDefaultAdapter();

$authAdapter = new Zend_Auth_Adapter_DbTable($dbAdapter);

 

$authAdapter->setTableName('zendLogin')

->setIdentityColumn('username')

->setCredentialColumn('password')

->setCredentialTreatment('MD5(?)');

2. 对象初始化后,就能够调用

$auth->setIdentity(username);

$auth->setCredential(password);

$result=$auth->authenticate();

If($result->isValid()){

// the default storage is a sessionwith namespace Zend_Auth

$authStorage = $auth->getStorage();

$authStorage->write($userInfo);

}

进行用户的认证了。

3. 而在用户退出时则可使用:

Zend_Auth::getInstance()->clearIdentity();

清除保存的认证信息。

 

(在用户进行成功经过认证以后,须要为其设置一个持久的认证,这在Zend_Auth中默认使用Zend_Auth_Storage_Session实现认证信息保存功能。)

7. Zend_Log

在Zend Framework中给出了一个通用性的日志组件,Zend_Log,它负责进行各类日志保存的操做,它又分为

Log对象:Zend_Log的实例,经常使用的对象

Write对象:继承与Zend_Log_Writer_Abstract,负责向存储中保存数据

Filter对象:实现了Zend_Log_Filter_Interface,过滤一些不须要的数据

Formatter对象:实现了Zend_Log_Formatter_Interface,在Write写入数据以前对日志数据进行格式化工做,每一个Writer只能有一个Formatter对象

在下面只介绍Zend_Log对象。

大致使用方法:

$writer=newZend_Log_Writer_Stream(‘log.txt’);//初始化Zend_Log_Writer_Stream实例

$log=newZend_Log($writer); //初始化Zend_Log对象

$log->log(‘someitems’ Zend_Log::ALERT); //向日志中写入数据,使用alert消息级别,可使用的消息级别有不少…具体查网上吧。。

相关文章
相关标签/搜索