zoukankan      html  css  js  c++  java
  • TP3.2详解

    一、框架概述

    1、什么是框架

    所谓的框架,Framework,它就是某个web应用程序的半成品,就是一组组件(分页类、验证码类、文件上传类、DB类、Image类等。。。),利用这些组件完成自己的web应用系统。

     

    现在企业中开发模式基本有三种:

    从快到慢排序:二次开发(需要修改源码)--->框架开发---->源码开发

    现在企业开发较多的模式还是框架开发

    2、框架开发的优势

    l 框架能节省开发时间

    每个项目中常用的类库都已经封装好,如分页类、图像类,文件上传类等,直接调用即可,非常方便。(减少重复造轮子

    l 利于团队的合作开发

    因为框架都是mvc设计模式,模块划分清晰,更有利于项目的分工合作,从而提高开发效率。

    3、框架的设计模式(MVC)

    基本现在主流的框架(CITPlaravelYIIzend frameworksymfony,cakephp),都是采用单一入口(index.php),且都是采用mvc设计模式,把一个web应用程序分为三块,控制器、模型、视图。

    C-Controller 控制器

    主要作用:就是接收用户的请求,在模型和视图之间起一个调度的作用

    M-Model 模型

    作用:对数据库做增删改查的

    V-View视图

    展示模板内容,给用户一个可操作的界面

    mvc形象理解-酒店:

     

     

     

     

     

     

     

     

     

     

    开发模式的发展大致经历以下三种模式:

    混合开发:phphtml代码混在一起.

    优点:速度快

    缺点:维护不方便

    模板开发(smarty:将业务逻辑(控制器)和html代码分开

    优点:将业务逻辑和html代码分开

    缺点:数据逻辑(模型)和业务逻辑混在一起

    框架开发(优点大于缺点):将一个应用程序分为mvc三层

    优点:结构非常清晰,有利于分工和维护

    缺点:运行速度慢,需要加载很多文件.

    4、常见的php开发框架

    ThinkPHP:简称TP,国人开发的一款比较优秀的mvc设计模式的框架

     

    CodeIgniter:简称CI,

     

    YII:主要开发大型的web应用程序:

     

    yii中有一个脚手架工具,可以生成很多文件。

    laravel:据说是最”优雅”的框架

     

    Zend Frameworkphp官方提供的框架,非常笨重

     

     

    框架只有最合适的,没有最好的,需要结合自己的系统和业务去选择最合适的。

    二、ThinkPHP框架项目的部署

    1、框架的下载

    下载地址: http://www.thinkphp.cn/down.html  

     

    2、项目部署

    目的:主要使用ThinkPHP搭建一个文章管理系统,学会使用ThinkPHP,TP框架核心语法和功能用熟,在结合我们之前ajax+jquery+插件知识全部都结合在一起。

    部署步骤:

    1、给项目配置一个虚拟主机,并修改host文件完成ip和域名的映射

     

    最后记得重启apache无服务器

    2、部署ThinkPHP框架到项目中去

     

    访问,出现如下的一个笑脸即可:

    3、部署成功后的一些细节

    1、应用目录的自动生成

     

    2、框架的核心目录说明

     

    3、为什么会出现如下的一个笑脸

    那是因为受到ThinkPHP核心配置的影响conf/convention.php,默认访问HOME模块下面的Index控制器的index方法。

     

     

     

    三、控制器

    1、控制器的介绍

    C-Controller

    作用:主要在模型和视图之前起一个调度的作用,接收用户的请求。

    2、控制器的定义

    定义控制器的规则:

    控制器名+Controller

    定义控制器的文件

    控制器名+Controller.class.php    // 首字母大写

    快捷键Ctrl+鼠标左键追踪源代码的执行过程

    如在HOME模块下面建立一个Test控制器,并建立一个index的方法

     

    访问如下:

     

    3、URL访问模式

    有以下四种url模式:

     

    3-1、普通模式

     

    3-2pathinfo模式

     

    方式一:

     

    方式二:pathinfo(重点掌握)

     

    3-3、重写模式(隐藏index.php入口文件)

     

    需要把apacherewrite模块给开启和把虚拟主机中none改为all,最后重启apache即可。

    访问形式如下:

     

    3-4,兼容模式

     

    以上有四种url模式,只需要掌握pathinfo模式。

    4、模块的分组

    4-1、添加分组

    直接复制Home模块即可,记得改命名空间即可。

     

    4-2、设置默认组

    ThinkPHP默认是访问HOME分组,我们修改配置文件。修改为默认是admin分组,配置文件

     

    4-3、模块的权限控制

    我们可以设置哪些模块是不可以和不可以访问,配置一下文件即可:

     

    5、跳转和重定向

    5-1跳转

    成功跳转:$this->success(“msg”,’index’,time);

    建议写法:$this->success(“msg”,U(“index”),time);

    失败跳转:$this->error(“msg”,’url’,time); //一般只给第一个参数

    参数说明:

    msg:操作失败或成功的提示信息

    url:跳转的地址,一般都是写当前控制器中某一个方法, 如果是error,此参数可以不写,默认跳回上一页(history.back(-1)

    time:跳转的时间。succes默认是1s,error默认是3s

    5-2、重定向

    基本格式:

     

    一般跨控制器或者跨分组建议直接使用重定向redirect,或者直接跳转不想给任何的提示信息也可以直接使用redirect()

     

    6、空控制器和空操作(方法)

    空控制器:EmptyController

    空操作: _empty

    主要作用: 主要用来做404的错误页面提示。

     

    四、视图

    1、视图的介绍

    v-view

    作用:主要输出模板内容的,给用户一个可操作的界面

    2、分配变量和输出模板内容

     

    分配变量:

     

    输出模板内容:

    $this->display(“模板名”);  //如果模板名没有设置,默认去模块下面View/控制器同名目录/操作名.html 的模板内容

     

     

     

    模板输出:

    结果: 

     

     

    fetchdisplay的区别,他们都是经过三个步骤:

    display():①获取模板内容 ②把分配的变量进行替换 ③输出(echo)替换好的模板内容

    fetch():①返回模板内容 ②把分配的变量进行替换 ③返回(return)替换好的模板内容

     

    3、标签

    3-1foreachvolist标签

    foreachvolist都是主要用来循环数组的,只是volsit功能更多点。

     

    代码:

     

    模板循环数据:

     

    结果:

     

    3-2include标签

     作用:一般网站的头部和尾部都是一样,我们可以把这块内容给抽离出来写一个html的公共的文件,在使用的时候直接用include包含进来即可,方便维护。

     

    结果:

     

    3-3if判断标签

     

    3-4php标签

     

    特别注意:

     

    4、系统变量

     

    代码:

     

    模板中获取:

     

    访问:

     

    5、常量替换

     

    这些模板替换主要是替换成url路径,方便后面在模板中直接使用。一般用于ahrefformaction.

    使用替换常量进行跳转:

     

    以上的常量会替换成指定url路径:

     

    除了以上的方式还是使用ThinkPHP提供的内置函数U()进行跳转。

     

    结果:

     

    不同的URL_MODEL配置,对我们的U方法生成的路径也是不一样的。下面以URL_MODEL等于3为例:

     

    使用U()在模板中进行跳转:

     

    注意一点:在模板中使用函数需要在函数名前面加一个冒号:

    使用U方法success成功函数中第二个参数中使用

     

    五、搭建后台的登录页面

    1、admin模块中添加一个PublicController.class.php文件,并且添加一个login的方法,输出模板内容

     

    2、login登录模板复制到admin/view/Public/目录下面,并且把资源文件复制到网站根目录下面的public文件夹中

     

    定义一个模板替换常量,进行资源文件的统一替换:

     

    模板中login.html进行路径的替换:

    效果:

     

    六、搭建后台的首页

    1、admin后台建立一个IndexControler.class.php的控制器,并建立一个index的方法,输出模板内容

     

    2、把首页的头部和菜单抽离出来

     

    引入公共页面:

     

    替换资源路径:

     

    效果:

     

    七、模板视图中使用函数

     

    代码:

     

    模板中函数处理:

     

    结果:

     

    默认函数default的使用:

     

    结果:

    八、单字母方法

    C(),I()

    1、I()主要是获取用户请求的参数

     

    提示:在模板中原生php代码中也可以使用框架提供的内置函数,如下所示:

     

    结果:

     

    2、C():主要是获取配置文件中的参数

     

    配置文件的优先级,当前模块(Admin>公共模块(Common>框架核心配置(convention.php

     

    效果:

     

    九、模型

    1、模型的认识

    M-Model

    作用:主要是操作数据库,对数据库中的数据做一些操作(CURD

    2、模型的定义

    记住:一个数据表对应着一个模型类,一类的功能都是对应着一个控制器

    如:表名(加表前缀) tp_category

    模型的定义: CategoryModel

    模型文件文件: CategoryModel.class.php

    如果一个表名含有下划线,模型类名采用驼峰法命名,如表名为 tp_goods_photo ,模型类名GoodsPhotoModel ,文件名GoodsPhotoModel.class.php

    3、建表

     

    4、连接数据库

     

    5、实例化模型

    一般的话一个表就对应着一个模型,但是不是必须的。没有表模型也是可以对数据表进行进本的CURD(增删改查)操作

    给表tp_category设置自定义类模型:

     

    实例化模型:

     

    也可以通过命名空间实例化模型:

     

    6、D方法和M方法的区别

    M(“表名”):实例化系统父类model  ,命名空间为:ThinkModel

    D(“自定义类名”):实例化我们自定义类名 , 命名空间为:AdminModel类名Model

    D方法功能更强大,因为后面我会在自定义类模型中添加很多方法和设置,这时候只能通过实例化D()去调用。

    十、模型的curd

    c-create 添加数据

    u-update 更新数据

    r-read 读取数据

    d-delete 删除数据

    7-1、添加数据C操作

    方法:$model->add(数组) ,新增成功之后返回记录的主键值

     

    一般一个订单记录会对应多个订单商品,如果想插入多条记录可以ThinkPHP提供的addAll的方法完成批量数据的插入,插入成功返回第一条记录的ID

     

    7-2、删除数据d操作

    $model->delete(主键值);删除记录成功返回受影响的行数

    $model->delete(1); 删除主键值为1的记录

    $model->delete(1,4); 删除主键值为14的记录

    $model->delete(); //删除失败 ,必须要指定主键值

     

    7-3、更新数据U操作

    $model->save(数组),成功返回受影响的行数

     

    7-4、查询数据R操作

    $model->Select();  如果没有指定主键值,查询表中所有的数据,成功返回二维数组

    $model->Select(1,3); 返回主键值为13的记录,

    $model->find(2); 查询主键值2的记录,成功返回一维数组

     

    select语句查询成功是返回二维数组,下标是从0开始,这种下标对我们并什么用,我们可以指定某些字段作为二维数组的下标,一般都是使用主键字段作为下标值。

    $model->select(array(“index”=>’字段名’));

     

    结果:

     

    7-5、原生的sql语句执行

     

    原生的查询语句:

     

    结果:

     

    原生的增删改:

     

    十一、模型的连贯操作方法

     

    特别注意:

    以上的连贯操作方法都是返回当前模型的对象,也就是说这些方法最后一定是return $this,也就是说他们之间没有调用的先后顺序,如果是查询操作,语句的最后面一定要保证是select或者是find方法。

    原生的sql语句基本顺序:where--->order--->group---->having---->limit

    查看select的使用语法:

     

    代码操作:

     

    结果:

     

    建议where的查询条件是数组的形式:

     

    结果:

     

    模糊查询like:

     

    结果:

     

    联表join查询:

    原生sql语句查询出栏目的父栏目名称:

     

    原生sql语句查询文章的所属分类名称:

     

    原生sql语句查询出栏目的父栏目名称:

     

    结果:

     

    使用join方法链表查询出文章的所属分类:

     

    结果:

     

    十二、统计函数

     

    代码:

     

    结果:

     

    十三、表名的操作

    一般而言,建议把表名(不含表前缀)和模型类名称保持一致,但是当模型类名和表名不一样的时候一般有以下两种处理方式:

     

    使用tableName的情况:

    表名:tp_mycategory

    模型类名:category

     

    使用trueTableName的情况:

    表名:mycategory

    模型类名:category

     

    特别:不管是使用tableName还是trueTableName,使用D()实例化自定义模型的时候,D()方法传入的值还是模型的类名,如下所示:

     

    十四、AR模式

     

    AR核心:

    ①表映射到类(模型类)

    ②记录就映射到对象

    ③字段映射到对象的属性

    使用AR 默认完成CURD

    ar模式的添加

     

    ar模式的更新

     

    ③ar模式的删除

     

    ar模式的查询

     

    十五、用户登录的实现

    1、修改login.html页面

     

    2、登录的处理逻辑,在模型中建立一个checkuser的方法,检查用户名和密码是否匹配

    3、控制器验证是否登录成功,把登录成功的信息写在session

     

    4、登录成功需要把用户的信息显示在首页(修改公共页面menu.html

     

    十六、完成登录的验证码实现

    1、在后台的PublicControler.class.php中定义一个生成验证码的方法genCode.

     

    2、login.html模板页面中需要单击验证码变化

     

     

     

    3、public控制器的登录的方法中判断验证码是否正确

     

    效果:

     

    作业:分析核心控制器的构造函数如下红色框所示:

     

     

    十七、完成用户的退出功能

    核心思想:清除用户登录成功的session信息,清除之后直接重定向到登录页面去

    1、给公共页面header设置一个退出登录的连接

     

    2、在后台的public的控制器定义一个退出登录的方法

     

    十八、防止用户翻墙

    核心思想:定义一个公共的控制器CommonController,在此控制器中的(_initialize)方法做session的统一验证,然后其他需要验证权限的控制器继承此公共控制器即可。

    1、分析核心控制器的构造函数的某些代码

     

    40-41行:首先判断当前对象所处在的类是否有定义_initialize的方法,如果有定义直接执行。

     

    2、在公共控制器CommonController定义一个session验证的方法

     

    注意:success跳转的时候,代码会继续往下执行,没有终止,可以在后面die掉,或exit()

     

    3、需要session验证的控制需要继承公共控制器

     

     

     

     

     

    十九、会话技术

    1、为什么会有会话技术

    因为http无状态性,客户端请求服务器的时候,服务器没法记住是哪台客户端,所以为了即可客户端浏览器,就有sessioncookie会话技术。

    2、session的基本操作(CURD)

    注意一点:在ThinkPHPsession会话是自动开启的,可以通过以下的配置进行设置。

     

     

    代码中实现:

     

    3、cookie的基本操作(CURD)

     

     

    代码中实现:

     

    二十、调试

    1、变量调试dump()

     

    2、开启页面trace调试

    需要在配置文件中开启以下配置:

     

     

    页面的右下角查看信息:

     

    3、sql语句的调试(模型的调试)

     

    分析模型的父类ThinkModel.class.phpselect方法:

     

    二十一、文件的载入

    1、只要是在模块中(Common,自定义模块Admin,Home)Common文件夹中建立一个function.php,然后再里面写一些我们自己的方法,此类文件会被框架自动加载,那么在代码的任何地方都是可以访问其中的方法。

     

    2、如果在模块的Common文件夹中文件名不叫function.php,a.php,这类文件我们需要通过配置文件加载:

     

    以上文件虽然可以被载入成功,但是如果访问其他控制器的某个方法,这些文件中的函数如果没有使用到的话也会被加载到内存中去,能不能按需加载?

     

     

    有没有载入文件成功,可以通过页面trace调试可以查看到:

     

     

     

    二十二:字段缓存和字段定义

    1、字段缓存

    只要在入口文件中开启调试模型(debug==>true,实例化模型(D())的时候都会使用一个sql语句SHOW COLUMNS FROM `tp_category`去分析我们的表结构,这种分析师要耗费磁盘i/o开销的。

    如果是关闭调试模式(debug==>false,那么实例化模型的时候,会在Runtime/data目录中生成对应模型的字段缓存文件。

    所以开不开启调试模式,都是操作磁盘i/o去获取文件的信息。

    代码如下:

    1、开启调试模式:

     

    打印实例化模型对象:

     

    结果:每次访问此test方法的时候,实例化模型都会分析表结构:

     

    2、关闭调试模式:

     

    再次打印模型的时候是不会在分析表结构:

     

    结论:不管在入口文件index.php是否开启调试模式,都是要操作磁盘文件,那么就会有磁盘的i/o开销。

    解决办法:通过字段定义。在模型中把表中的所有的字段都定义好,下次就不会有磁盘的i/o开销,直接从字段定义中获取了。

    2、字段定义

     

    代码中实现:去CateModel模型中定义字段定义

     

    建议:主要给表设置了一个表模型,就最好给模型定义好字段定义。可以就不会有磁盘i/o开销

    二十三、完成文章分类的添加

    1、修改公共的页面menu,加一个添加文章分类的链接

     

    2、在后台admin建立一个CategoryController的控制器,完成栏目的添加

     

    模板位置:

     

    模板引入时间插件:

     

    时间初始化:

     

    效果:

     

     

    3、封装一个无限极分类的方法

     

     

     

    效果:

     

     

     

    二十四、通过创建数据对象完成栏目的添加

     

    1、创建数据对象$model->create();

     

    995-996:判断有没有传递数据,如果没有传递数据,则把post表单中的所有的数据分配给变量$data

    继续分析源代码:

     

    1065:把上面变量$data赋值给当前模型($catModel)的属性data.

    1067: 把变量$data返回,create()方法可以接收,做其他的业务逻辑。

    分析模型的add的方法:

     

    301-304行:首先判断有没有给add传递参数,如果没有则从模型的data属性中获取,赋值给变量$data;

    继续往下分析:

     

    316insert方法入库之前先执行入库前的钩子_before_insert($data);

    320:把上面接收到$data进行入库。

    2、模型的自动验证

    1、介绍

     

    2、基本语法

     

    分析create源代码,分析自动验证:

     

    如果没有验证通过就会返回false,控制器调用create就可以进行判断。判断没有通过就可以通过模型调用$model->getError()获取没有验证通过的提示信息。

    3、栏目的入库操作

    1、栏目模型:

     

    2、栏目的添加入库:

     

    4、模型的自动完成

    1、介绍

     

    2、基本语法

     

    我们之前通过入库前的钩子_before_insert()完成时间的添加,还可以通过模型自动完成也可以实现此功能。

    在栏目的模型中定义好一个自动完成:

     

    分析creat的底层代码:

     

    5、总结create、自动验证、自动完成、钩子函数

     

     

    6、使用验证完成登录的验证

    1、模型中定义验证规则

     

    2、控制器验证,也需要传对应的验证类型4

    二十四、使用验证插件完成分类的添加验证

    前端验证:减少后台服务器的压力。

    后端验证:为了数据的安全。

    1、载入核心验证的js文件(jquery.validate.js

     

    给表单绑定一个属性id,并初始化验证规则:

     

    扩展:给插件扩充自定义的验证规则

     

    效果:

     

    二十五、文章栏目的列表

    1、修改后台链接地址

     

    2、 在后台的category控制器建立一个index,获取分类的数据,并且输出模板内容

     

    如果不联表的话需要把无限极分类的函数返回的二维数组的下标也要改为cat_id,否则下标从0开始,造成数据错误。

     

    3、模板中显示数据

     

    效果:

     

    二十六、文章的栏目的编辑的功能

    1、修改编辑模板index.html的内容,加一个链接地址

     

     

    2、Category控制器新建一个upd的方法完成分类的编辑

     

    3模型中添加一个更新前的钩子,完成时间的修改操作

     

    3、模板页面upd.html

     

    上级栏目默认选中:

    方式一:

     

    方式二:

     

    效果:

     

     

     

    作业:编辑的时候,不能让当前栏目或子孙栏目作为父栏目。

     

    二十七、完成分类的无刷新删除

    需要注意的地方:

    ①删除分类的时候,当前栏目下面有子分类不能删除

    ②删除分类的时候,还需要判断分类下面有没有文章

    1、给删除的连接设置一个属性cat_idclass=”del”

     

    2、ajax删除逻辑

     

    3、category控制器删除逻辑

     

    4、Category模型中判断一个栏目有没有子分类和文章

     

    作业:无刷新删除成功后,文章分类列表的序号需要重新排列

     

    二十八、完成文章的添加

    1、在后台的AricleController控制器中添加一个add的方法,显示添加的模板内容

     

    2、定义articleModel完成添加文章的自动验证和插入前钩子完成时间的入库

     

    3、赋值一份add.html模板到admin/view/Article

     

    4、使用插件layui富文本编辑器完成文章内容添加

    a.引入cssjs

     

    引入js

     

    c.textarea元素绑定一个id,如demo

     

    d.初始化textarea

     

    效果:

     

     

     

    二十九、完成文章的图片上传

    1、定义表单form的属性,必须是post提交且含有enctype属性

     

    图片预览实现,需要引入js文件,和给input绑定onchange事件:

     

    效果:

     

     

    2、在入库前的钩子函数(_before_insert)中判断有没有上传图片,进行图片的上传

     

    模型中定义一个文件上传的方法:

     

    说明:如果没有上传成功,则把上传的错误信息赋值给当前模型对象的一个error属性,然后再控制器那边通过模型对象调用getError获取到错误信息提示给用户。

     

    三十、完成文章的图片缩略图生成

    1、在入库前的钩子函数中进行图片的缩略图处理

     

    存储的图片路径:

     

    数据库存储的路径:

     

    三十一、完成文章的列表

    1、在后台的articleController建立一个index的方法取出所有的文章,并且输出模板内容

     

    2、模板中分配数据,复制栏目的index.html模板

     

    效果:

     

    三十二、完成文章的列表传统分页

    1、articleController控制器的index的方法完成分页的功能

     

    2、模板中显示

     

    效果:

     

    注意:默认ThinkPHP的分页列码是没有样式的,但是每个超链接都有对应的class 属性,我们可以通过这些class给其设置样式

    引入page.css文件,并给div加一个class值为pagination

     

     

    效果:

     

    三十三、完成文章的无刷新分页

    分析:

     

    主要核心思想:当点击分页的时候,只需要从后台获取到分页的数据和分页的列码传回给前端页面,在进行替换即可。

    问题是怎么获取分页的列码和分页的数据?

    分页的列码程序可以通过获取到,通过以下方式:

    分页的数据如何获取到? 

    可以把分页的主体数据给抽离出来单独保存一个html页面,然后在使用fetct()方法获取此模板中内容。

    fetchdisplay的区别,他们都是经过三个步骤:

    display():①获取模板内容 ②把分配的变量进行替换 ③输出(echo)替换好的模板内容

    fetch():①获取模板内容 ②把分配的变量进行替换 ③返回(return)替换好的模板内容

    实现步骤:

     1、把分页的主体数据给抽离出来 ,保存为一个data.html模板文件

     

    2、给页码的超链接绑定单击事件(一定要用on去绑定)

     

    3、ArticleController控制器判断是否是ajax请求,把分页的主体和分页的页码数据以json格式输出

     

     

    三十四、作业讲解

    1、无刷新删除序号重新排列的问题

     

    解决办法:首先删除成功之后获取tbody下面的所有的tr,each(k,v)循环,每次循环的时候给tr的第一个td的设置设置为k+1;

    代码实现:

     

    2、编辑分类

     

    代码操作:

    1、在控制器中根据当前栏目的id获取子孙栏目id

     

    2、在模型中定义获取子分类id的方法

     

    三十四、使用ajax+弹出层完成文章内容的查看

    1、给查看文章详情绑定一个class=showContent’属性和自定义属性article_id

     

    2、class=showConteng绑定一个单击事件(需要用on去绑定)

     

    3、控制器获取文章的详情(需要把html实体符号转化为标签)

     

    效果:

     

    三十五、鼠标悬浮图片放大

    1、修改data.html页面

     

    css样式:

     

    效果:

     

    三十六、文章的搜索功能

     

    1、给搜索区域套一个form标签,并且是get方式提交

    {$Think.get.keyword}回显数据

     

    2、控制器组装查询条件

     

    效果:

     

    三十七、使用统计图表插件完成文章数量的统计

    1、下载下来,引入两个核心的js文件

     

    2、给文章列表页加一个功能按钮,统计文章分类中文章的总数量

     

    3、控制器查询出每个分类的文章的总数量

     

    4、模板中构造数据

     

    组装好的数据放置指定位置即可:

     

    效果:

     

  • 相关阅读:
    UVALive 5983 MAGRID DP
    2015暑假训练(UVALive 5983
    poj 1426 Find The Multiple (BFS)
    poj 3126 Prime Path (BFS)
    poj 2251 Dungeon Master 3维bfs(水水)
    poj 3278 catch that cow BFS(基础水)
    poj3083 Children of the Candy Corn BFS&&DFS
    BZOJ1878: [SDOI2009]HH的项链 (离线查询+树状数组)
    洛谷P3178 [HAOI2015]树上操作(dfs序+线段树)
    洛谷P3065 [USACO12DEC]第一!First!(Trie树+拓扑排序)
  • 原文地址:https://www.cnblogs.com/wangyuming/p/7439588.html
Copyright © 2011-2022 走看看