巨蟒django之权限8:排序&&菜单展开权限归属

1.权限控制的流程+表结构

内容回顾:

wsgi:socket进行收发消息

中间件:(超级重点的面试题)在全局范围内控制django的输入和输出的一个钩子,处理输入和输出说白了就是处理请求和响应request对象和response对象,他说的是一个全局的钩子,认为是所有的请求都要进来,钩子的概念是什么?只要把功能写上去就能运行,中间件注册上就能用,注销了,整个东西就没有了,可插拔性非常好,写好了就能用,没写好就不能用,提前预留好了.说到这里,我们知道form里边有局部钩子和全局钩子,

session是将数据存储到内存中,数据的读取会非常的快.连接数据库的话也是可以的,每次登陆都需要进行读取一遍,速度回慢一些,压力比较大.

下面,我们分析一下上边的流程,看一下都完成了哪些事情,第一次,假如说是没有登录的话,直接访问某个页面,比如说是customer_list,浏览器进来,走wsgi.封装成request对象,来走中间件,中间件里边定义了一些权限控制的中间件,中间件里边有(白名单),白名单没有通过校验,进入登录状态的校验,没有登录状态,这个时候就会返回页面,也就是redirect重定向,本质上就是一个响应头,返回给了浏览器,浏览器看到之后,再次向login发送一个登录的get请求,请求进来先走wsgi,再走过了中间件的白名单,通过白名单,向后走,走到urls.py匹配,访问login的视图函数,执行view里边的视图函数,然后打印模板,渲染一个login的登录页面,这样往回走,走到中间件的时候,需要再走所有中间件的process_response方法,虽然我们写的中间函数中没有,但是其他中间件中可能有,再走wsgi,再回到浏览器.浏览器就可以看到登录页面,登录页面中输入用户名和密码,发送POST请求,再走wsgi,然后走中间件,依然是login白名单,再走urls.py,然后走views.py的视图函数,在views.py进行校验用户名和密码,这个时候,走model,然后从数据库中db进行查询,查询之后,返回查询的结果,数据库返回一行行数据,有了models之后,才封装成models对象,这个时候在view.py拿到models对象,因此,我们自己创建的models对象,和在数据库中通过ORM拿到的models对象是一样的,区别是,自己创建的话,自己填充数据,ORM操作是从数据库中拿到数据,然后封装成对象,自己封装的对象.save会保存到数据库中,或者拿到一个对象修改它的属性,再去save,会到数据库中进行修改,没用查找对象,认证失败,返回给浏览器需要重新登录,再次发POST请求.假如认证成功了,权限信息的初始化,保存的信息有哪些,保存权限的list,保存登录状态,保存菜单的dict,存入session中,返回重定向,这个时候,给的是index走到浏览器返回的是location,

 地址写的是index,浏览器再次发送一个请求index,走wsgi,再走中间件,获取到地址是index,白名单校验一下,发现不是白名单,接着往下走,但是

是一个登录状态,接着往后走,发现是一个免认证的地址,然后return,再走到urls.py,再次走到view视图中,返回一个index页面,这个时候用到了母版和继承,继承了layout.html,layout里边有我们自定义的menu标签,{% menu request%},这个地方用到了inclusiontag,结果是动态的html字码段,通过代码块,在大模板里边套了一个小母版,然后放在母版里边了,最后通过index进行继承,再把index写的字符串,添加到block块里边,最后我们拿到,大模板套着小母版,这个html页面,然后返回给浏览器,这个时候就用到了,动态生成菜单的结果,用到的是inclusiontag,这个时候浏览器显示的是有权限的信息了,点开的是一级菜单,二级菜单是我们可以访问的一些权限信息,然后点击二级菜单,先看图

点击进入一级菜单,显示二级菜单,显示我们可以访问的一些权限,我们再点击二级菜单,开始发送请求,再走wsgi,再走中间件,一次通过(白名单,登录状态的校验,免认证的地址)都不是,下面我们开始进行权限的校验,需要,获取当前用户的权限信息,这个时候,从session中拿到权限信息,然后和当前的地址,做一一的匹配,能匹配成功说明有这个权限,return就往回走,如果都没有匹配成功,就没有权限,就直接返回了给浏览器,这个是权限的控制,正常情况下有菜单是有权限的,我们通过菜单点出来,都是有权限的,往后走,比如"视图",再通过model,再拿数据库中的数据,返回一个queryset一个对象列表,然后我们再套模板,然后再渲染用户的一个个数据,用到了模板的继承,这个时候layout和{%menu request%}再次使用了,生成一个完整的html页面,再返回给浏览器,这个时候可以看到用户展示的列表,剩下的页面都是同样的效果,

 代码有哪几部分?

中间件和rbac相关的,所以把中间件写在rbac里边,

 登录用户和密码,这是登录相关的业务逻辑,需要写在业务的app里边,

 

权限信息的初始化和业务逻辑是没有太多关系的,只不过是登录成功之后所做的事情,我们也把这部分代码拿出来,放在rbac的app里边,

只要能找到,调用使用就可以了,这个时候我们是需要动态生成菜单的也就是  母版和继承&&layout&&{%menu request%}

这个html也是和权限rbac相关的,根据权限生成的菜单,把这部分也写在rbac的template里边的tags,也叫rbac进行区别,也就是和控制相关的,

还有一个关键的东西,settings.py,进行相关的配置.

 

访问127.0.0.1:8000相当于是访问127.0.0.1:8000/,(也就是指的是访问根目录)这个地址是没有访问权限的,

 

我们访问的是127.0.0.1:8000/login/地址,通过白名单之后拿到的页面.

输入root用户&&密码123

下面我们梳理下这个流程:

首先我们看中间件,也就是settings.py,

django帮助我们封装的中间件,请求进来通过这个中间件之后,才会封装成request.session,出去的时候,在视图中用到了request.session赋值,也就是在视图中出去时,再次经过request_session,会对用户进行设置,用到这个时候,我们需要注销的,

 csrf是用来做校验的,

最下面的rbac是我们自己注册的中间件,我们需要看下这个流程.
下面是settings.py里边的一些设置

我们向login发送post请求用户名和密码,

第一个白名单url匹配成功就return,再路由匹配,也就是项目的根的路由匹配,admin不是,访问这个空的

点击进入include里边的urls,我们将就进入二级url里边看一下,

找到路径之后,我们进入"视图",

 如果用户名或密码错了,下面的obj就是None,结果就会返回登录页面,我们需要重新登录login,显示"用户名或密码错误"

通过密码和用户了,我们就需要惊醒"权限信息初始化(权限,菜单)",点击进入init_permission函数

obj代表用户对象,obj.roles代表管理对象,帮助我们管理多对多的关系对象,

后边调用filter或者all才能拿到queryset,将角色的权限为空的权限去除掉

 思考,多表查询的方式,有哪几种?

  子查询,查询完之后再去另一张表中查询,

  连表查询:内连接,左连接,右连接,

  内连接只会罗列出:两张表中都有的信息,

  左连接是左边有右边没有,右边就补空,

目前,我们的情况是,就是补空的形式,角色表有这个形式,对应的权限没有id,和角色id对应匹配上,所以数据局势空,也就是这个filter里边为空的设置,要去除掉空的数据(url权限为空的部分),然后我们通过values取对应字段的值.最后的结果就是queryset,里边的形式就是一个字典,

找权限表里边的url和title,以及通过全下表再找到菜单表里边的title&&icon&&id.

 

上图中,再往下走是,构建权限的列表permission_list,菜单字典menu_dict

最终我们把结果存放到session当中了,

我们将session存放的键,放在settings.py中

 

感觉绕的话,也可以写死,但是我们可以通过配置,优化了程序,和上边一样

下面,我们看一下构建的数据结构:

permission_query指的就是每一个字典的权限信息,

下边一个构建的是权限列表,一个是构建的菜单字典

 上边就是构建一级和二级菜单的地址

 

初始化,完成之后,我们再重定向到index首页

这个时候我们再访问中间件rbac.py

 

url=index...,白名单也不是,获取登录状态,也已经在permission.py里边设置过了,然后,

再看一下,是不是免认证的状态,如果是里面认证的,在settings.py里边有免认证的地址,

然后return,往后走,路由的匹配,先项目,在二级路由,找到视图函数index.py

在index页面里边,我们先继承,然后写钩子里边的东西:

用上图所示的内容,相当于调用相关的函数,menu,执行menu,点击menu进去查看一下,在

menu_dict就是获取菜单的数据结构:

 得到下图这样的字典:

上图包含一级和二级菜单的信息,

 然后我们返回菜单字典里的值

 也就是找到装饰器里边的menu.html进行循环出来

生成菜单列表之后,我们就将生成的结果放在母版页layout.html里边的,下图对应的位置:

上图中的menu指的是函数名.这样就包含模板里边生成的内容

 同时看到的首页,也就会将index填充到layout.html中对应的block对应的前端位置.如下图

然后交还给浏览器,就可以看到对应的页面

 

二级菜单的难点:1构建数据结构,2inclusion_tag优点难度(记住这个inclusion_filter||inclusion_simple)记住用法和显示的结果就可以了,三个方法其实都是模板中调用函数3.表结构和对应的含义4.对应的流程图

发出一个请求对应一个响应,与下次请求没有关系,如果想要有关系,需要保存一个session 5.以及模板里边的对应关系等等.

 

今日内容:

(1)一级菜单排序

(2)二级菜单选中并且展开

(3)非菜单权限的归属

(4)路径导航

(5)权限粒度控制到按钮级别

 

2.一级菜单排序

标准:什么样的权限在上边,什么样的权限在下边,这个需要我们先规定好,如何处理?需要有依据,加东西,一级菜单排序

原来的菜单表样子:

下面,我们需要对它加上对应的权重,进行排序,

 

二级菜单结构对应的样子,如下图:

修改之后的样子:

只需要修改数据库对应的权重大小,就可以修改对应的顺序.

 

我们是在menu.html中循环生成的,循环下图字典中1和2对应的值

 

目前的问题:字典是无序的

python3.5字典是无序的

python3.6字典显示是有序的,内部是无序的

python3.7才真正的是和插入顺序是一致的.

为了保证字典有序,我们需要用到有序字典模块.

collections包中的orderedDict类  form表单中的setfields,也就是定义字段中的顺序,后边是字段对象.

 

字典的排序:

按照插入顺序排序,如何排序呢?sorted()

sorted默认是升序排列,加上reverse=True就是降序排列

 

 

上图我们循环的是menu_list里边的键,将顺序按照键来进行排序

我们现在的需求是按照wight来进行排序

,如何处理?结合匿名函数处理下面的一些关系,按照从大到小排序

修改参数,再看一下

 

如何实现这个功能呢?思考一会儿

首先执行数据库迁移

我们需要将右侧的数据库移除,重新拖动进去sqllites,然后打开menu表,修改一下权重,将1改成100

建议中间空上几个值,方便插入其他的菜单

 下面我们处理顺序,注意这个时候我们是在自定义的里边进行处理,

原来的样子:

 

 在permission中再加上一个筛选的权重wight,

 这个时候,我们按照降序排列,数值大的在前面

然后赋值,循环就可以了,另一个就是,我们现在返回的值是有序字典的值

这个时候,运行,重新登录一下root,我们看一下结果:(运行报错,将上边的ret=的里边的weight改成wight)

 这个时候,财务管理就在上边了,

 

 

我们现再修改一下数据库的权重,刷新页面看一下结果

结果没有发生改变,原因是session中的数据,不会因为你修改数据库就修改session里边的数据.

只有再次登录才会修改结果,下图是重新登录之后的结果:

二级菜单如果想排序也是可以加上权重,构造的时候,需要将下图的数据排好才行

 

 

3.二级菜单默认选中并展开

首先,我们拿当前的url地址,然后循环每一个标签的地址,匹配上我们就加active,这样就结束了,现在的情况是一级菜单套二级菜单,现在我们想拿二级菜单里边的url地址

原来的写法:

item代表一级菜单的结果,我们需要循环二级菜单的结果,看下面的写法:

item里边的i代表二级菜单里边的一个个字典,然后正则匹配.

现在我们拿到的是children里边的字典,见下图:拿下面的url

 

下面我们开始匹配当前的地址,拿到之后加上active

循环完成之后,我们再交给menu.html页面进行渲染

匹配完成之后,我们再处理,

运行程序:

解释:现在我们做的效果是,在原来的基础上在浏览器点击到二级菜单,浏览器的搜索框添加上对应的搜索地址

 

现在我们得到的结果默认,都是展开的状态

需要在body上加上hide

也就是在menu菜单里边加上hide

这个时候,所有的都是闭合的,

因此,hide我们不能写死,想要去掉,需要用js去掉

我们可以默认是展开的,然后进行操作

下图中item代表一级菜单的信息,我们可以设置

现在我们在一级菜单都加上hide

下面的两个菜单都是hide,

现在也是表示默认都是关闭的

现在我们应该访问哪个页面,哪个页面就应该是展开的状态,也就是访问二级菜单,应该显示的是选中的状态.

也就是,现在我们进去了,hide改成空字符串""

这个时候,我么再运行程序,选中的页面展开,

我们需要的效果是,点击就打开,点击另一个,就关上上一个打开的菜单.

我们需要通过js实现代码

原来的js代码:

 

运行程序:

js也在其他地方可能用到,也是需要拿出来的

这个时候,我们只需要在模板页中引入即可,我们就成功修改了二级菜单的问题

 

4.非菜单权限的归属

我们点击上图中的"添加客户",得到下图:

这个时候,我们不希望菜单栏关上,

 

左侧菜单:

客户管理

  展示客户 (一对多的关系)

    添加客户

    编辑客户

    删除客户

    ...

  财务管理

    缴费列表

    ...

  权限表:

  id   url          title      menu_id      parent_id

  1  /customer/list/     展示客户  5(菜单id的1)   null

   2  /customer/add/     添加客户  null       1(id为1)

  3  /customer/edit/(d+)     编辑客户  null      1

我们现在需要做的是产生关系在  展示客户和添加客户之间

并且是1对多的关系.

我们让"展示客户"成为"父选项",添加,编辑,删除,成为子选项

 思考,如何实现上边的表结构?

我们需要做路由匹配:

我们需要做的事情是,不管是访问什么只要找到:   添加和编辑删除的url对应的父亲就可以了,

我们的目的是:找到父亲url就可以了,

我们访问二级菜单,找到对应的url就可以了

点击客户,找到对应的父权限.不管是访问二级菜单还是子权限,我们只需要找到对应的父亲就可以了

自己关联自己1对多

下面运行,数据库迁移

 

现在,我们在admin里边添加权限信息

 

 

下面,我们开始获取了

在这里只是拿自己的关系字段:

下面我们在"菜单字典"里边添加内容

 

 

我们打印一下二级菜单里边的内容

服务端信息

这个时候,我们就拿到了二级菜单的权限信息,

 

 在菜单里边,我们需要进行配置,菜单里边已经有了,我们需要加上自己的id

 

我们再次访问这个地址:

得到的结果:

也就是url的地址.

再走展示客户的地址

 

 

 

 

我们再重新登录一下:

 

现在,我们得到的是二级菜单里边的内容:

我们现在,访问"展示客户"里边的内容:

 服务端得到的结果:

 

倒数第三行的id是二级菜单的id,

 我们再看一下url地址的匹配:

服务端对应所示的内容:

 

 

我们在这里的i拿到的是中间件的权限信息,

需要获取这个东西

我们获取的是两种请求的方式字典

 第一种访问的是"父权限",第二种是"子权限"

我们需要的是父权限的id,应该如何处理这个问题?

 

这个时候,我们保存的就是二级菜单的id

我们再将存储的id和打印的id对比一下.

 

 

i在这里表示的是二级菜单的字典,

我们就将这里的二级菜单的id和我们刚才存储的id做比较.

比对成功,我们就将下图的地址做相关的展示

 

 原来的代码:

修改之后的代码:进行比较

 

 思路,我们不管是获取的是二级菜单还是二级菜单里边的内容,都要把二级菜单获取到

 这个时候的小问题:(访问index的时候,我们找不到)

 

 走中间件,从上到下,从"白名单"到"获取登录状态",再到免认证,最后走到下图所示的内容:

因此,我们需要在一开始定义一个默认值,

这个时候,我们再刷新:

 

 我们再设置一个settings.py里边的一个配置

 

思考一下,这个时候,走到中间件应该怎样用?

中间件配置完成再用反射进行设置

 

这个时候,我们再在自定义的rbac里边取值

 

这样我们就将程写活了,

我们只是换了一种写法,

 反射的方法很方便,但是反射的流程会复杂一些,也可以先写死,多看几遍再写.

71-5最后2分钟复原,如果需要的话

 回复之前的样子:也可以反着回复过去

 

 

 

 

5.

下午回顾:

我们思考一下能不能优化一下程序:

原来的样子:

优化后的样子:

上边方块是循环前,下边是循环后的代码块

 两连等:

复习的内容:

1. 一级菜单的排序
    有序字典
    sorted(menu_dict, key=lambda x: menu_dict[x]['wight'], reverse=True)
2. 二级菜单选中并且展开
        hide  active 
    3. 非菜单权限的归属
        
        客户管理
            展示客户
                添加客户
                编辑客户
                删除客户
        财务管理
            缴费列表
        权限表
            id  url                           title     menu_id   parent_id
            1   /customer/list/                展示客户      5       null
            2   /customer/add/                 添加客户    null        1
            3   /customer/edit/(\d+)/          编辑客户    null        1

 

5.路径导航,也就是下图中的花红框的内容:

我们需要添加url地址和标题

我们需要将写死的变成动态的导航效果:

 

 

 

 

 这个时候layout.html就不再写死了,将下图红框内容注释掉

 

 

i是定义的字典,下面我们通过循环出来.

 

当走到上图的权限校验,我们就知道访问的是哪个地址了

 

服务端里边没有title,所以我们需要加上,见下图

 

 下图是我们筛选出的title信息

 

 

上图中的权限列表加上title,

这个时候,我们重新登录一下root

这个时候,已经可以成功显示了

这个时候,我们看到已经存在了title

这个时候,我们再点击"添加缴费记录"

这个时候就没有了

因为这个时候走的是if里边,见下图

下面我们进行尝试,把面刀导航栏也加到子权限中,看结果

见上图,现在就有了"添加缴费"

 

见上图,"编辑缴费"也有了

现在唯一少的是父亲的导航标签

 

 通过打印,我们可以看到部分添加到列表的信息

 打印的位置是中间件,基于角色的权限访问控制

 

如何通过服务端的信息拿二级菜单的title.

如何更方便的取到?

思考,可不可以将列表换成字典?

将上图的id当做key

看一下组织数据结构的思路:

从上边的结构修改成下边的结果,会更好处理一些

 

下面我们将权限的列表修改成权限的字典

 

原来列表的添加方式:

现在的添加的方式:

 

 下图这个session中存储的地方要修改成权限的字典

 

 

必须改成这样,否则下边会报错

存储的时候会产生影响,自然取的时候也会产生影响,

中间件rbac需要修改的内容:

修改后的结果:

这个时候两者都用到,我们就将语句放在最下边

运行:

原因是我们没有登录:

现在我们重新登录root&&123试一下

 

报错:

 

 

 

 

 

 

以上是一些错误原因,需要进行修改:

这个时候,再运行,

登录:

 

 

点击"添加客户"

 

也就是我们拿pid的时候出错了

拿到的是1,为什么报错?

细节问题

我们将data1这样的一个字典,存储到session中,做序列化,原来的情况是数字,我们现在做了json的序列化之后,字典的键是数字的话,会转化成"字符串",

 见下图:

这个时候,原来的情况变成了字符串了,我们对中间件进行操作:

 

这个时候,我们再次点击"添加客户"

 

 成功得到下图:

我们将导航栏写在,母版页面

我们需要再定义一个inclusion_tag,和上边的菜单栏是一样的

 

 

应该如何操作呢?

 

 

 

 现在我们只需要拿breadlist就可以了

 

 因为上边已经写了,load,我们只需要写自定义函数的请求就可以了

这个时候,我们刷新页面:

 

现在我们存在的问题是小问题是,在当前页面的时候不需要,点击"缴费列表",到最后一个不用点就行了

 我们只需要修改循环的次数,进行判断:

 

 

这样最后一个就没有a标签了,通过id判断

通过子权限找到父权限的信息,

我们将权限列表修改成了权限字典,json字符串的转化,最后一个面包屑的位置没有a标签

 

6.权限粒度控制到按钮级别

我们需要做的是有权限就展示,没有权限就不展示了,这个就是权限粒度控制到按钮级别

我们现在需要做的是对应用户能够展示的结果:

我们将所有的权限放到一个列表当中,if和else进行判断

 拿什么代表权限?url

什么和url有关系?

路由系统里边的name也能代表一个url

我们将用户所拥有的的name收集起来,

 我们还可以通过customer_list.html进行相关的反向解析,url和name是1对1对应的.

 如果在列表中,我们就进行展示,没有在列表中我们就不进行展示

 

 name 现在我们思考应该怎么处理?

我们需要在rbac文件夹下的models.py里边的Permission类(也就是表)中,再添加一个属性.但是这个名字必须唯一,如果表中已经创建了,不要先加唯一,需要进行一些处理

 我们先演示一下上边操作之后的错误显示,

1)需要在提供一个一次性默认值,

2)需要再添加一个default

我们选择1

随便写一个"xx"

 

报错:

报错

因为我们向数据库中存储数据有约束,所以,因此我们不能给url默认值,不能是一样的

 

 我们先删掉,数据库迁移里边的name

 

 

我们再将上边的name的唯一值去掉,

再次执行命令,以及提供默认值

 

 

再执行上图,我们将成功生成到数据库当中去了,

这个时候,当前的所有字段都是xxx

这个时候,我们在url中,先加上name之后,我们再加上约束

我们知道web当中的urls.py

 

 

 我们将别名一次添加到权限列表中:

点击上传:

处理完成之后,我们再添加约束

如果表是空的,我们一开始就能添加,但是我们的表不是空的,需要先加上默认值,再加上修改之后的不同值,也就是别名之后,

我们需要再添加unique=True

 这个时候,我们再执行命令:

这个时候,就没有问题了.

这样我们就把数据保存起来了,我们现在需要找的是列表或者集合进行处理

 

 我们可以在permission中再次添加一个数据结构,添加之后保存在session中,但是没有必要,我们还可以怎样操作呢?

我们现在拿到的是权限的id做的是key,见下图,

id的特点是唯一,不重复.name加上unique相当于是唯一的,不重复的,我们也可以吧name当做key进行处理.

 

我们需要将权限id,修改成权限name

原来的代码

现在的代码:

以后我们可以通过keys拿到所有的权限了

修改之后,存在一个问题,路径导航会找不到了,原因是什么?

原因是数据结构发生了改变:这个时候从"子权限"找"父权限"已经找不到了

我们可以换一种方式,找parent_name

下图是修改之后的数据结构:

 查询下面的权限表中的父亲,然后找名字name,是两个下划线

 

注意,后边的"逗号"一定要加上

这个时候就查询拿到父权限的name,

下面,我们在打印一下构建的字典:

登录root&&123

运行:http://127.0.0.1:8000/login/

 登录root&&123

 

服务器端,这个时候可以得到字典:

 

 这个时候,我们得到的data2和data3是一样的

现在我们再修改一下中间件内部的信息

修改之后的内容:

 

 展示客户没有问题:

添加客户也没有问题:

注释,下边两行的内容:

现在,我们直接判断字典:

我们看一下settings.py中的信息

 

 

上边,我们做的是,如果有这个权限就显示,没有这个权限,我们就不显示

运行,登录qiangge&&123账户

登录之后,只有一个"展示客户"的权限

我们将权限的别名当做字典的key

 上边的if end写法写死了,下面我们开始,用filter进行再次定义一下

我们再将a i a注释掉

刷新:

登录root账户,依然得到的是下面的结果:

我们现在,还是只需要,做简单的if判断就行了

我们再定义一个,

 

 

如果两个都没有权限,如何处理?

编辑和删除,存在一个我们就显示,不存在就不再显示了

选项里边,也需要加上这部分内容:

这个时候,我们再次登录qiangge&&123,看一下显示

 

这个时候,因为没有权限,就不在显示"编辑"和删除了

我们给"秘书",再添加一个"编辑用户",点击保存.

再次登录qiangge&&123

这个时候,就可以登陆了

粒度显示,就是做if...else判断,有就显示,没有就不显示,表结构上的改变,name也就是别名,记录url,并且不能重复,有了这个条件,我们就可以做字典中的key了

修改上图的时候,会对"子权限"找"父权限",产生影响,这个时候,我们用pname找,也可以找到了,

 

 

 我们自定义的这个filter方法,就需要判断当前的name是不是在权限字典里边,如果存在,直接return,不存在,则None,

需要生成页面的话,尤其是需要控制到按钮级别,就需要将判断一个一个加到按钮链接上边,我们需要对每一个按钮做判断,我们做的事情就是麻烦的事情.

,还需要大力复习,前面的知识点.

 

posted @ 2019-03-21 17:20  studybrother  阅读(436)  评论(0编辑  收藏  举报