ZendFramework 入门教程

一、Zend Framework简介

1. 什么是ZendFramework

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

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

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

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

2. 什么是LAMP

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

为什么要说LAMP呢,因为这些软件的组合强大到只要提到一个就必须要提到另外三个的地步,而在实验室网站的建设中,我们使用PHP,我们采取的当然也是LAMP的框架

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多种平台,其安装也是非常简单的,和正常软件一样,只要进行下载,运行软件即可。

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

Zend安装和使用

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

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

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才可,否则<?=?>的表达式不能被正常解析。

二、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控件则提供了各种更强大的功能,大家可以自己发掘,这里不再赘述

三、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消息级别,可以使用的消息级别有很多…具体查网上吧。。

注:本文转自:http://blog.csdn.net/onlysis/article/details/7062630

posted @ 2012-05-16 12:35  简道云  阅读(17443)  评论(1编辑  收藏  举报