前期内容提要:
- 【抽奖平台开发(1)】抽奖功能的前端实现(HTML+JS+CSS)
- 【抽奖平台开发(2)】抽奖结果的表单提交,实现Web前后端的数据交互(HTML+JS+PHP)
- 【抽奖平台开发(3)】将抽奖结果提交的表单上传至数据库,完成抽奖平台前台开发(PHP+MySQL)
在完成抽奖平台前台部分的全部功能搭建与测试后,为一进步完善抽奖平台的使用体验,降低抽奖数据管理门槛,保障数据库安全,应当改善现阶段管理员通过进入数据库后台对抽奖结果予以管理的操作模式,开发并搭建一个与前台配套的抽奖平台后台,用以实现抽奖结果查看与管理操作的可视化。
基本思路:作为数据管理后台,一方面应当支持管理员查看 抽奖列表(包括中奖时间和奖品名称)、奖品详细信息(如奖品价格,调货地址,购买商电话等等),因此需要优化后台可管理信息种类;另一方面应当允许管理员对于数据进行操作,基于MVC思想,完成抽奖平台的后台开发。
需求 | 数据获取方式 | 后台数据呈现方法 | 触发机制 |
---|---|---|---|
显示“中奖时间” | 抽奖前端表单提交数据至数据库 | 直接调取数据库存储的信息 | 自动触发 |
显示“奖品名称” | 抽奖前端表单提交数据至数据库 | 直接调取数据库存储的信息 | 自动触发 |
查询“奖品详细信息” | 数据库预设 | 直接调取数据库人工预设的信息 | 点击触发 |
统计“数据总数” | 使用数据库命令语句 | 使用数据库“查询语句” | 自动触发 |
数据操作之“删除数据” | 使用数据库命令语句 | 使用数据库“删除语句” | 点击触发 |
一、后台MVC
开发模式的选择及开发思路
后台的主要功能在于管理数据库数据,为管理员提供一个安全高效可视化的数据管理平台。后台的全部数据从数据库中调取,管理员在后台上的操作发挥数据库操作语句的作用。传统的显示与逻辑混合没有办法实现使用各种不同样式的视图来访问同一个服务器端的代码,管理员操作权限划分工程量及数据存储量巨大;此外,后台数据管理平台的搭建是一个循序渐进的过程,需要实现一种动态的程序设计,以便于后续对程序的修改和扩展简化,并且使程序某一部分的重复利用成为可能。基于此,选择MVC开发模式,将显示与逻辑相分离,通过对复杂度的简化,使程序结构更加直观,同时提高代码复用率,降低耦合度。
所谓MVC全名是Model View Controller,是模型(model)-视图(view)-控制器(controller)的缩写,模型Model层管理大部分的业务逻辑和所有的数据库逻辑。模型提供了连接和操作数据库的抽象层;控制器Controller层负责响应用户请求、准备数据,以及决定如何展示数据;视图View层负责渲染数据,通过HTML方式呈现给用户。
简单通俗的讲:Model层就是对数据库的操作;View层用于从后台获取的数据通过页面予以呈现;Controller层作为枢纽,在View层和Model层之间存在,接收到用户的请求,将模型和视图匹配在一起,侦听由View层(或其他外部源)触发的事件,并对这些事件执行适当的反应。在大多数情况下,反应是在Model层上调用方法。
从用户层面来看,用户通过浏览器直接访问Controller层,Controller层接受用户请求后调用Model完成状态的读写操作并将数据传送给View层予以响应,View层渲染最终结果并呈现给用户。此时用户在View层查看数据,当用户在View层发出数据操作请求时,Controller层将截获用户发出的操作请求调用Model完成操作后将操作结果(一般表现为数据的变动,如用户删除数据表中的一行数据)传递给View,View层再次渲染最终结果(即删除一行数据后的数据表)并呈现给用户,值此循环往复。
将MVC开发模式运用到抽奖平台后台开发上来,如上图所示。用户通过浏览器访问index.php
进入Controller层,在Controller层的Controller.php
文件中,定义一个基于用户不同请求(查看抽奖列表、中奖详情、删除数据)调用相对应模型后返回视图页面的控制器。此后则是进入Model层通过Model.php
实现数据的操作。数据的操作除了命令语句的发出,还需要数据库的连接与响应,这一步骤就交给BaseModel.class.php
(基础模型类)和MySQLDB.class.php
(mysql数据库操作类)进行处理,此外,为保证系统中一个类只有一个实例,通过ModelFactory.class.php
完成单例工厂的搭建以实现模型类的单例。最后Controller层的控制器调用与用户请求相对应的模型实现用户的操作请求后将通过View层返回视图页面,即View-index.html
(“显示抽奖列表”、“删除抽奖信息”操作的视图返回页面)与View-GiftInfo.html
(“查看抽奖结果详情”操作的视图返回页面)。
文件结构:
/www/wwwroot/***.com
└── 后台
├── index.php
├── Framework
│ ├── ModelFactory.class.php
│ ├── BaseModel.class.php
│ └── MySQLDB.class.php
└── Application
├── Controller
│ └── Controller.php
├── Model
│ └── Model.php
└── Views
├── View-index.html
└── View-GiftInfo.html
二、基于后台需求修复前台缺陷
在真正开始后台搭建前,基于后台的信息管理需求,有必要对前台部分功能予以完善。在前三章中,我们只是简单实现了中奖结果
的前后端传输,并不符合真实情形下高度系统化的管理需求。正如前文关于平台需求所述,在实际工程应用中,后台管理员显然不应该仅仅能够看到中奖结果
,诸如抽奖用户信息
、中奖时间
、奖品详细信息
(比如奖品价格,调货地址,购买商电话等等)等要素必然也是后台查看和管理的重要元素。而这类元素又大致可以分为两类,一类依赖于用户的前台操作,我们需要利用表单形式将此类操作数据传输至后端(如中奖结果
、抽奖用户信息
、中奖时间
);另一类则不依赖于用户的前台操作,只需要在数据库内进行必要的预设后可以直接在后台调取数据库内存储的相关信息即可(如奖品详细信息
),与前台操作无关。
因此在这一节中,我们需要实现的是通过PHP将第一类元素数据(中奖结果
、抽奖用户信息
、中奖时间
)传输至后端。在这里我们仅抽选中奖时间
和中奖结果
两条数据,用以演示。
(由于我在前台并未设置
抽奖用户信息
录入页面,因此不传输抽奖用户信息
,其实现方法是设计一个抽奖用户信息
录入页面,通过表单形式将录入信息提交至PHP即可,十分简单,故也不再多述了)
1. 重新配置数据库结构
这里我设置了三个字段名,分别为id
、times
和gift
,并将id
设置为 主键 并且 AUTO_INCREMENT ,分别用来排序
(唯一标示符),记录中奖时间
以及中奖内容
。
之所以需要在数据表中加入一个自动增长的主键ID,其目的一方面是为了定位每一条数据以便后续的数据操作命令的正确执行,另一方面则是出于对管理员删除数据操作的监控,在数据删除后,其数据所对应的唯一ID也将删除,编号连续性中断,这就要求管理员审慎的对待数据管理与操作。当然,如果后期新增事务处理则需要予以进一步的改进。
2. 在前台PHP中获取“中奖时间”和“中奖内容”
<?php
$tm = date('Y-m-d H:i:s',time());
$results=$_GET['results'];
echo "Time:" . $tm ."<br>";
echo "Gift lists:" . $results ."<br>";
?>
3. 存储至数据库
<?php
header("content-type:text/html;charset=utf-8");
@ $db=mysqli_connect("localhost","用户名","密码","数据库名称");
if(mysqli_connect_errno()){
echo("Error:Couldnot connect the database");
exit;
}
/在获取“中奖时间”和“中奖内容”后:
$strsql = "insert into gift(times,gift) values('$tm',$results)";
$result=mysqli_query($db,$strsql);
if(!$result){
echo("fail to insert data");
}else{
echo("sucess in insert data");
}
@ mysqli_free_result($result);
mysqli_close($db);
?>
4. 测试
三、平台共用部分(Framework)开发
在明晰开发思路与文件结构后,我们正式开始后台的搭建工作。首先是整个平台公共部分的开发。根据开发思路,为保证系统中一个类只有一个实例,通过ModelFactory.class.php
完成单例工厂的搭建以实现模型类的单例;此外由于用户的所有操作是基于数据库信息管理的操作,这一套步骤由BaseModel.class.php
(基础模型类)和MySQLDB.class.php
(mysql数据库操作类)完成。
1. ModelFactory.class.php
为防止重复实例化,减少消耗系统和内存的资源,有且仅有一个实例对象,我们常选择单例模式。所谓单例模式,是一种常用的软件设计模式,在它的核心结构中只包含一个被称为单例的特殊类。单例模式会阻止其他对象实例化其自己的单例对象的副本,从而确保所有对象都访问唯一实例。由于在Model层我们无法保证模型类本身是单例,因此有必要设计出一个“单例工厂类”,通过该单例工厂类,去“获取”模型类的类名,并返回给类的一个实例(对象),以确保单例。
<?php
class ModelFactory{
Static $all_model = array(); //用于存储各个模型类的唯一实例(单例)
Static function M( $model_name ){ //$model_name是一个模型类的类名
if( !isset(static::$all_model[$model_name]) //如果不存在
||
!( static::$all_model[$model_name] instanceof $model_name ) //或不是其实例
)
{
static::$all_model[$model_name] = new $model_name();
}
return static::$all_model[$model_name];
}
}
2. MySQLDB.class.php
(mysql数据库操作类)
用于定义数据库操作方法,添加数据管理功能,通过该类的对象实现:
(1)执行任意的增删改语句
(2)执行返回一行/多行/一个数据(用于计数)的“查询语句”(分别返回“一维数组”、“二维数组”、“数据值”)
(3)执行任何sql语句,并进行错误处理,或返回执行结果
<?php
class MySQLDB{
private $link = null; //用于存储连接成功后的“资源”
private $host;
private $port;
private $user;
private $pass;
private $charset;
private $dbname;
private static $instance = null;
static function GetInstance($config){
if( !(self::$instance instanceof self) ){
self::$instance = new self($config);
}
return self::$instance;
}
private function __clone(){}
private function __construct($config){
$this->host = $config['host'];
$this->port = $config['port'];
$this->user = $config['user'];
$this->pass = $config['pass'];
$this->dbname = $config['dbname'];
$this->charset = !empty($config['charset']) ? $config['charset'] : "utf8" ;
$this->link = mysqli_connect("{$this->host}:{$this->port}", "{$this->user}", "{$this->pass}","{$this->dbname}")
or die("连接失败");
}
//这个方法实现连接关闭
function closeDB(){
mysqli_close($this->link);
}
//这个方法为了执行一条增删改语句,它可以返回真假结果。
function exec($sql){
$result = $this->query($sql);
return true;
}
//这个方法是返回一行数据的“查询语句”,它可以返回一维数组
function GetOneRow($sql){
$result = $this->query($sql);
$rec = mysqli_fetch_assoc( $result );//取出第一行数据(其实应该只有这一行)
mysqli_free_result( $result ); //提前释放资源(销毁结果集),否则需要等到页面结束才自动销毁
return $rec;
}
//这个方法是返回多行数据的“查询语句”,它可以返回二维数组
function GetRows($sql){
$result = $this->query($sql);
$arr = array(); //空数组,用于存放要返回的结果数组(二维)
while ( $rec = mysqli_fetch_assoc( $result ) ){
$arr[] = $rec; //此时,$arr就是二维数组了!
}
mysqli_free_result( $result );
return $arr;
}
//这个方法是返回一个数据的“查询语句”,它可以返回一个直接值
function GetOneData($sql){
$result = $this->query($sql);
$rec = mysqli_fetch_row( $result );
$data = $rec[0];
mysqli_free_result( $result );
return $data;
}
//统筹上面的所有方法,用于执行任何sql语句,并进行错误处理,或返回执行结果;
private function query( $sql ){
$result = mysqli_query($this->link,$sql);
if( $result === false){
echo "<p>sql语句执行失败,请参考如下信息:";
echo "<br />错误代号:" . mysql_errno();
echo "<br />错误信息:" . mysql_error();
echo "<br />错误语句:" . $sql;
die();
}
return $result; //返回的是“执行结果”
}
}
?>
3. BaseModel.class.php
(基础模型类,现阶段仅需用于存储数据库账号密码)
<?php
class BaseModel{
//用于存储数据库工具类的实例(对象)
protected _dao = null;
function __construct(){
$config = array(
'host' => "localhost",
'port' => 3306,
'user' => "用户名",
'pass' => "密码",
'charset' => "utf8",
'dbname' => "数据库名称"
);
$this->_dao = MySQLDB::GetInstance($config);
}
}
此处的dao
,指Data Access Object
(数据访问对象)。在基础模型类中使用MySQLDB工具类
($this->_dao = MySQLDB::GetInstance($config)
,其中$_dao
属性是从MySQLDB工具类
处获得的实例,返回的是一个单例对象),工具类在实例化之后由$_dao
属性来存储对象。
四、Controller层开发(PHP)
在完成整个平台公共部分的开发后,我们进入Controller层开发,根据开发思路,用户通过浏览器访问index.php
进入Controller层,在Controller层的Controller.php
文件中,定义一个基于用户不同请求调用相对应模型后返回视图页面的控制器。
/www/wwwroot/***.com
└── 后台
├── index.php
└── Application
└── Controller
└── Controller.php
1. index.php
:
这开发过程中,需要时刻保持分工负责的思维模式,index.php
作为用户通过浏览器进入Controller层的入口,只发挥环境配置和引导作用,不应赋予其过多功能。
<?php
header("content-type:text/html; charset=utf-8");//设置输出的字符编码为utf8
require './Framework/MySQLDB.class.php';
require './Framework/ModelFactory.class.php';
require './Framework/BaseModel.class.php';
require './Application/Modelss/Model.php';
require './Application/Controllers/Controller.php';
$ctrl = new Controller();
$act = !empty($_GET['act']) ? $_GET['act'] : "Index";
$action = $act . "Action";
$ctrl->$action(); //可变函数————>>可变方法
?>
2. Controller.php
:
在Controller层的Controller.php
文件中,定义一个基于用户不同请求(“显示抽奖列表”、“删除抽奖信息”、“查看抽奖结果详情”)调用相对应模型后返回视图页面的控制器。
<?php
class Controller{
//显示抽奖列表
function IndexAction(){
$obj_List = ModelFactory::M('ListModel');
$data1 = $obj_List->GetAllList(); //是一个二维数组
$data2 = $obj_List->GetListCount(); //是一个数字
include './Application/Views/View-index.html';
}
//删除指定抽奖信息
function DelAction(){
$id = $_GET['id'];
$obj = ModelFactory::M('ListModel');
$result = $obj->delListById($id);
echo "<script>alert('删除成功!')</script>";
echo "<meta http-equiv='Refresh' content='0;URL=index.php'>";
}
//查看指定抽奖结果详情
function DetailAction(){
$id = $_GET['id'];
$obj = ModelFactory::M('ListModel');
$data = $obj->GetGiftInfoById($id);
include './Application/Views/View-GiftInfo.html';
}
}
在这里需要额外补充强调几点:
index.php
中的语句含义:代替了Controller.php
内所有if判断逻辑,即:
if(!empty($_GET['act']) && $_GET['act'] == 'detail'){
DetailAction();
}
else if(!empty($_GET['act']) && $_GET['act'] == 'del'){
DelAction();
}
else{
IndexAction();
}
- 载入视图文件的逻辑起点:
根据文件结构,Controller.php
需要回退至Application
文件夹后再进入Views
文件夹最后才能载入视图文件,但是需要注意的是,所有逻辑起点都应当从index.php
开始,所以不需要考虑要回退多少个目录,加多少个"."。
Controller.php
中删除操作后的跳转问题:
echo "<script>alert('删除成功!')</script>";
echo "<meta http-equiv='Refresh' content='0;URL=index.php'>";
在删除数据成功后,页面自动跳转回入口页面,目的一方面在于保障数据结构安全,另一方面在于回归逻辑层面的初始状态,保障操作逻辑的完整与统一。
$_GET
安全性问题:
很显然,直接使用未经封装的$_GET
拼接在SQL上存在XSS和SQL注入风险,对于有安全性需求的用户,建议尝试在php.ini中开启magic_quotes_gpc
对于所有由用户GET、POST、COOKIE中传入的特殊字符予以转义或者使用mysql_real_escape_string()
函数进行过滤再使用以预防上述安全隐患。最简单粗暴的做法可以用htmlspecialchars
把特殊字符(&,",',<,>)
转换为HTML实体(&"'<>)
后输出。
五、Models层开发(PHP)
所有模型都需要继承基础模型类,即前文的BaseModel.class.php
,在此基础上,Model层通过执行数据库类方法,将执行结果返回至Controller.php
。
Model.php
:
<?php
class ListModel extends BaseModel{
function GetAllList(){
$sql = "select * from gift;";
$data = $this->_dao->GetRows($sql);
return $data;
}
function GetListCount(){
$sql = "select count(*) as c from gift;";
$data = $this->_dao->GetOneData($sql);
return $data;
}
function delListById($id){
$sql = "delete from gift where id = $id;";
$data = $this->_dao->exec($sql);
return $data;
}
function GetGiftInfoById($id){
$sql = "select * from gift where id = $id;";
$data = $this->_dao->GetOneRow($sql);
return $data;
}
}
其中GetRows($sql)
、GetOneData($sql)
、GetOneData($sql)
、exec($sql)
是MySQLDB工具类的方法,_dao
则是从父类BaseModel
中$_dao
属性继承而来的用于存储的对象,是MySQLDB工具类的实例。
六、Views层开发(HTML)
当Controller层的控制器调用与用户请求相对应的模型实现用户的操作请求后将通过View层返回视图页面。
需求 | 后台数据呈现方法 | 触发机制 |
---|---|---|
显示“中奖时间” | 直接调取数据库存储的信息 | 自动触发 |
显示“奖品名称” | 直接调取数据库存储的信息 | 自动触发 |
查询“奖品详细信息” | 直接调取数据库人工预设的信息 | 点击触发 |
统计“数据总数” | 使用数据库“查询语句” | 自动触发 |
数据操作之“删除数据” | 使用数据库“删除语句” | 点击触发 |
从需求的触发机制来看,自动触发的数据操作可以安排在一个页面即View-index.html
予以呈现;需要用户点击触发的是 查询“奖品详细信息” 以及 数据操作之“删除数据” 两个操作,其中后者仅仅是数据库信息的管理操作,也可以直接在View-index.html
页面回显操作结果;唯一是 查询“奖品详细信息” 由于需要调取的是数据库内预设的信息(即与中奖奖品相对应的信息如奖品价格,调货地址,购买商电话等等),因此需要额外一个页面返回视图,即View-GiftInfo.html
。
1. View-index.html
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="zh-cn">
<head>
<meta http-equiv="Content-Type" content="text/html;charset=UTF-8" />
<title>抽奖结果查询后台</title>
<meta name="keywords" content="抽奖结果查询后台" />
<meta name="description" content="抽奖结果查询后台" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0">
<link rel="stylesheet" type="text/css" href="" />
<style type="text/css">
.t1{color:red;}
.t2{background:#cccccc}
</style>
<script type="text/javascript"></script>
</head>
<body>
<div style="position: absolute;left: 50%;transform:translateX(-50%);white-space:nowrap" >
<div style="font-size: 23px;font-weight: bold;text-align: center">抽奖结果</div>
<br />
<div class="fr">
<table border='1'>
<tr>
<th >编号</th>
<th >中奖时间</th>
<th >奖品内容</th>
<th >数据管理</th>
</tr>
<?php
foreach( $data1 as $key => $rec )
{
?>
<tr>
<td ><?php echo $rec['id']; ?></td>
<td class='t1'><?php echo $rec['times']; ?></td>
<td class='t2'><?php echo $rec['gift']; ?></td>
<td>
<a href='?act=del&id=<?php echo $rec['id']; ?>' οnclick='return queren()'>删除</a>
<a href='?act=detail&id=<?php echo $rec['id']; ?>' >详细</a>
</td>
</tr>
<?php
}
?>
</table>
<br />
当前抽奖记录总数:<?php echo $data2;?>
</div>
</div>
</body>
</html>
<script>
function queren(){
return window.confirm("是否确认删除?");
}
</script>
<script src="https://code.jquery.com/jquery-1.9.1.min.js"></script>
<script>
$(function(){
setInterval(aa,1000);
function aa(){
$(".fr").load(location.href+" .fr");
}
})
</script>
对于用户的抽奖结果,后台应当能够实时检测,手动刷新以同步数据库信息的方式显然不符合管理需求,对此实现方式大致有二,其一,设置定时器自动刷新页面以同步数据(实现方式见下方代码块);其二,不刷新页面更新数据,对于用户量小的平台可以基于Ajax实现轮询,用户量大可选择长轮询或workman Socket服务器框架。
<script>
function fresh_page(){
window.location.reload();
};
setTimeout('fresh_page()',10000);
</script>
具体应用到本后台的搭建中,由于在Views层是通过<?php echo $rec['**']; ?>
的形式更新数据,因此并不一定需要使用Ajax轮询,综合考虑页面整体刷新的局限性,在这里我选用了jQUery的局部刷新方法,将需要同步的内容包装在class="fr"
的div
标签中,通过div
的局部刷新以实时同步数据库信息至后台:
<div class="fr">
//需要同步的内容
</div>
<script src="https://code.jquery.com/jquery-1.9.1.min.js"></script>
<script>
$(function(){
setInterval(aa,1000);
function aa(){
$(".fr").load(location.href+" .fr");
}
})
</script>
另一方面,实现了响应式布局,以同时适配PC端和移动端的访问与操作;并使用<td>
标签以控制表格的跨行跨列。
2. View-GiftInfo.html
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="zh-cn">
<head>
<meta http-equiv="Content-Type" content="text/html;charset=UTF-8" />
<title>详细信息</title>
<meta name="keywords" content="详细信息" />
<meta name="description" content="详细信息" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0">
<link rel="stylesheet" type="text/css" href="" />
<style type="text/css">
</style>
<script type="text/javascript"></script>
</head>
<body>
<div style="position: absolute;left: 50%;transform:translateX(-50%)" >
<div style="font-size:23px;font-weight:bold;white-space:nowrap">关于编号<font color=red><?php echo $data['id'] ?></font>的详细信息如下:</div>
<hr />
<nobr />中奖时间:<?php echo $data['times'] ?>
<br/>
<br/>
<nobr />奖品名称:<?php echo $data['gift'] ?>
<br/>
<br/>
<nobr />奖品价格:<?php echo '信息暂未录入' ?>
<br/>
<br/>
<nobr />中奖者姓名:<?php echo '信息暂未录入' ?>
<br/>
<br/>
<nobr />中奖者联系方式:<?php echo '信息暂未录入' ?>
<hr />
<a href='?'>返回</a>
</div>
</body>
</html>
关于中奖奖品相对应的信息的录入工作,考虑到篇幅问题以及实现的简易程度,在此不再多述。
七、测试:
需求 | 触发机制 | 呈现页面 |
---|---|---|
显示“中奖时间” | 自动触发 | View-index.html |
显示“奖品名称” | 自动触发 | View-index.html |
统计“数据总数” | 自动触发 | View-index.html |
需求 | 触发机制 | 呈现页面 |
---|---|---|
数据操作之“删除数据” | 点击触发 | View-index.html |
查询“奖品详细信息” | 点击触发 | View-GiftInfo.html |
-
数据操作之“删除数据”
-
查询“奖品详细信息”
至此,我们成功完成了抽奖平台的全部开发(源码已上传)。
本系列文章索引目录:
- 【抽奖平台开发(1)】抽奖功能的前端实现(HTML+JS+CSS)
- 【抽奖平台开发(2)】抽奖结果的表单提交,实现Web前后端的数据交互(HTML+JS+PHP)
- 【抽奖平台开发(3)】将抽奖结果提交的表单上传至数据库,完成抽奖平台前台开发(PHP+MySQL)
- 【抽奖平台开发(4)】基于MVC模式实现数据后台管理操作的可视化(PHP+HTML+MySQL)
如果您有任何疑问或者好的建议,期待你的留言与评论!
转载:https://blog.csdn.net/deng_xj/article/details/100816981