车辆违章查询系统-基于Python-Django后端渲染项目

车辆违章查询系统-基于Python-Django后端渲染项目

作者:代昌松

项目详情代码参见:

后端渲染:https://gitee.com/dcstempt_ping/carsys_back_end_rendering

前后端分离:https://gitee.com/dcstempt_ping/TPIS

Django概述

Python的web框架中有上百个,比他的关键字还要多。所谓的Web框架,就是用于开发Web服务器端应用的基础设施,说得通俗一点就是一系列封装好的模块和工具。事实上,即便没有Web框架,我们仍然可以通过socket或CGI来开发服务端应用,但是这样做的成本和代价在商业项目中通常是不能接受的。通过Web框架,我们可以化繁为简,降低创建、更新、扩展应用程序的工作量。刚才我们说到Python中的Web框架有上百个,这些框架包括Django、Flask、Tornado、Sanic、Pyramid、Bottle、Web2py、web.py等等。

在上述Python的web框架中,Django无疑是最具有代表性的重量级选手,开发者可以基于Django快速地开发可靠的web应用程序,因为它减少了Web开发中不必要的开销,对于常用的设计和开发模式进行了封装,并对MVC架构提供了支持(Django中称之为MTV架构)。MVC是软件系统开发领域中一种放之四海而皆准的架构,它将系统中软件分为模型(Model)、视图(View)和控制器(Controller)三个部分并借此实现模型(数据)和视图(显示)的解耦合。由于模型和视图进行了分离,所以需要一个中间人将解耦合的模型和视图联系起来,扮演这个角色的就是控制器。稍具规模的软件都会使用MVC架构(或者是从MVC演化出的其他架构),DJango项目中我们称之为MTV,MTV中的M跟MVC中的M没有区别,就是代表数据模型,T代表网页模板(显示数据的视图),而V代表了视图函数,在Django框架中,视图函数和Django框架本身一起扮演了MVC中C的角色。
在这里插入图片描述
Django框架诞生于2003年,它是一个在真正的应用中成长起来的项目,由劳伦斯出版集团下在线新闻网站的内容管理系统(CMS)研发团队开发,以比利时的吉普赛爵士吉他手Django Reinhardt来命名。Django框架在2005年夏天作为开源框架项目发布,使用Django框架能够用很短的时间构建出功能完备的网站,因为它代替程序员完成了那些重复乏味的劳动,剩下真正有意义的核心业务给程序员来开发,这一点就是对DRY(Don‘t Repeat Yourself)理念的最好践行。许多成功的网站和应用都是基于Python语言进行开发的,国内比较具有代表性的网站包括:知乎、豆瓣网、果壳网、搜狐闪电邮箱、101围棋网、海报时尚网、背书吧、堆糖、手机搜狐网、咕咚、爱福窝、果库等,其中不乏使用了Django框架的产品。

创建Django项目

  1. 检查Python环境:Django1.11需要python2.7或python3.4以上的版本;Django2.0需要python3.4以上的版本;Django2.1和2.2需要python3.5以上的版本;Django3.0需要python3.6以上的版本。【说明】用户可以在windows系统命令题是符中输入python–version查看当前python的版本。

    python --version
    
  2. 更新包管理工具并安装Django环境(用于创建Django项目)

    pip install django==2.2.12 -i https://pypi.doubanio.com/simple
    # 通过==指定安装的版本;通过指定国内豆瓣镜像可以加快安装速度
    
  3. 检查Django环境并使用django-admin命令创建Django项目

    django--admin --version
    django-admin startproject carsys	#carsys为项目的名称
    
  4. 用pycharm打开创建好的新项目,并为其添加虚拟环境。
    在这里插入图片描述

    如上图所示,pycharm的项目浏览器中,最顶层的文件夹carsys是Python项目文件夹,这个文件夹的名字并不重要,Django项目也不关心这个文件夹叫什么名字。该文件夹下有一个同名的文件夹,它是Django项目文件夹,其中包含了 __init__.pysettings.pyurls.pywsgi.py,并且与名为carsys的django项目文件夹同级的还有一个名为manage.py,这些文件夹的作用如下所示:

    • carys/__init__.py:空文件夹,告诉python解释器这个目录应该被视为一个python的包

    • carsys/setting.py:Django项目的配置文件

    • carsys/urls.py:Django项目的URL映射声明,类似于网站的“目录”

    • carsys/wsgi.py:项目运行在WSGI兼容的Web服务器上的入口文件

      【说明】WSGI全称是Web服务器网关接口,维基百科上给出的解释是“为python语言定义的Web服务器和Web应用程序或框架之间的一种简单而通用的接口”。

      创建项目虚拟环境如下图:

在这里插入图片描述

5. 安装项目依赖项:打开pycharm的终端,通过pip命令安装Django项目的依赖项

    ```
    pip install django==2.2.12
    ```

    或者在pycharm的setting中找到项目解释器安装第三方库,注意安装Django依赖项的时候,需要指定版本号,否则将默认安装最新的Django第三方库

在这里插入图片描述

6. 启动Django自带的服务器运行初始项目,可在终端运行以下命令:

    ```python
    python manage.py runserver
    ```

    或者直接通过pycharm运行项目

在这里插入图片描述

7. 查看运行效果

    在浏览器中输入http://127.0.0.1:8000访问Django服务器,效果如下:

在这里插入图片描述

【说明】

  1. 刚刚启动的Django自带的服务器只能用于开发和测试环境,因为这个服务器是纯python编写的轻量级Web服务器,不适合在生产环境中使用

  2. 如果修改了代码,不需要为了让修改的代码重新生效而重启Django自带的服务器。但是在添加新项目文件时,该服务器不会自动重新加载,这时需要重启服务器

  3. 可以在终端中通过python manage.py help命令查看Django管理脚本可用的命令参数

  4. 使用python manage.py runserver启动服务器时,可以在后面添加参数来指定IP地址和端口号,默认情况下启动服务器将运行本机的8000端口

  5. 在终端中运行的服务器,可以通过Ctrl+C来停止它。通过pycharm的运行配置运行的服务器直接点击窗口上的关闭按钮就可以停止服务器运行

  6. 不能在同一个端口上启动多个服务器,会导致地址冲突(端口是对IP地址的扩展,也是计算机网络地址的一部分)

  7. 修改项目配置文件setting.py

    Django是一个支持国际化和本地化的框架,因此可以通过修改配置文件将默认语言改成中文,时区设置为东八区:

    找到配置文件的106行

    LANGUAGE_CODE = 'en-us'
    TIME_ZONE = 'UTC'
    

    修改为:

    LANGUAGE_CODE = 'zh-hans'
    TIME_ZONE = 'Asia/shanghai'
    

    刷新刚才的页面,可以看到修改语言和时区之后的结果
    在这里插入图片描述

基于后端渲染的Django车辆查询系统

创建Django应用

想要开发新的Web应用,需要在Django项目中创建应用,一个Django项目可以包含一个或多个应用

  1. 在pycharm终端中执行以下命令,创建名为polls的应用

    python manage.py startapp polls
    # 也可以用如下命令
    django-admin startapp polls
    

    执行了上面的命令会在当前路径下创建polls目录,其目录结构如下:

    • __init__.py:空文件夹,告诉python解释器这个目录应该被是为一个python的包
    • admin.py:可以用来注册模型,用于在Django框架自带的管理后台中管理模型
    • apps.py:当前应用的配置文件
    • migrations:存放与模型有关的数据库迁移信息
    • models.py:存放应用的数据模型(MVC中的M)
    • tests.py:包含测试应用各项功能的测试类和测试函数
    • views.py:处理用户HTTP请求并返回HTTP响应的函数或类(MVC中的V)

建立车辆违章查询系统

前边我们提到了Django是基于MVC架构的Web框架,MVC架构追求的是“模型”和“视图”的解耦合。所谓“模型”说得更直白一些就是数据,所以通常也称为“数据模型”。在实际的项目中,数据模型通常通过数据库持久化操作,而关系型数据库在过去和当下都是持久化的首选方案。

我们用Django搭建的车辆违章查询系统,包含以下几个功能

  1. 查询系统的首页只有一个查询按钮和搜索框
  2. 若搜索框为空,表示查询所有违章记录
  3. 若搜索框输入车牌号或车主姓名,可以显示相应的违章记录(包含模糊查询)
  4. 查询页面包含分页和翻页的功能
  5. 点击受理,可以将未受理的违章记录的状态修改为已受理
  6. 只可以对已经受理的违章记录进行删除
  7. 查询页面底部包含违章次数的条形统计图
  8. 实现Excal报表的导出

在这个项目中,我们使用MySQL数据库来实现数据持久化操作。由于项目的最终目标是实现前后端分离,在此之前,我们先通过后端渲染搭建Django项目。

首先,在polls目录中新建一个名为templates的模板页,并根据上边的需求,准备了查询页面的静态页index.heml,稍后会将静态页修改为Django项目所需的模板页,文件夹结构如下所示:
在这里插入图片描述

并修改setting.py文件将index.html纳入模板页

TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [os.path.join(BASE_DIR, 'templates'), ],
        'APP_DIRS': True,
        'OPTIONS': {
            'context_processors': [
                'django.template.context_processors.debug',
                'django.template.context_processors.request',
                'django.contrib.auth.context_processors.auth',
                'django.contrib.messages.context_processors.messages',
            ],
        },
    },
]

index.html模板页的初始代码:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>违章查询</title>
    <style>
        #result td {
            text-align: center;
        }
    </style>
</head>
<body>
    <div id="result">
        <form id="search" action="/search/" method="post">
            {% csrf_token %}
            <input type="text" name="carinfo" value="{{ carinfo }}" size="30" placeholder="请输入车牌号或车主姓名">
            <input type="submit" value="查询">
        </form>
        <hr>
        {% if records %}
        <table>
            <tr>
                <th width="100">车牌号</th>
                <th width="100">车主姓名</th>
                <th width="150">违章原因</th>
                <th width="180">违章时间</th>
                <th width="180">处罚方式</th>
                <th width="100">是否受理</th>
                <th width="120">操作</th>
            </tr>
            {% for record in records %}
            <tr>
                <td>{{ record.car.carno }}</td>
                <td>{{ record.car.owner }}</td>
                <td>{{ record.reason }}</td>
                <td>{{ record.makedate }}</td>
                <td>{{ record.punish }}</td>
                <td>{{ record.dealt | yesno:'已受理,未受理' }}</td>
                <td>
                    <a class="handle" href="/handle/?rno={{ record.no }}">受理</a>
                    <a class="delete" href="/delete/?rno={{ record.no }}">删除</a>
                </td>
            </tr>
            {% endfor %}
        </table>
        <div class="buttons">
            <button data="/search/?currentPage=1">首页</button>&nbsp;&nbsp;
            {% if has_prev %}<button data="/search/?currentPage={{ prev_page }}">上一页</button>&nbsp;&nbsp;
            {% else %}<button disabled>上一页</button>&nbsp;&nbsp;
            {% endif %}
            第{{ current_page }}页/共{{ total_page }}页&nbsp;&nbsp;
            {% if has_next %}<button data="/search/?currentPage={{ next_page }}">下一页</button>&nbsp;&nbsp;
            {% else %}<button disabled>下一页</button>&nbsp;&nbsp;
            {% endif %}
            <button data="/search/?currentPage={{ total_page }}">末页</button>
        </div>
        {% endif %}
    </div>
    <a id="export" href="/export/">导出Excel</a>
</body>
</html>

配置关系型数据库MySQL

  1. 在MySQL中创建数据库,创建用户,授权用户访问该数据库

    create database TPIS default charset utf8;
    create user 'TPIS'@'%' identified by '5201314@Dcs';
    grant all privileges on vote.* to 'TPIS';
    flush privileges;
    
  2. 在MySQL中创建车辆信息表和违章记录表

    use TPIS;
    
    create table `tb_car` (
    	`no` int(11) not null auto_increment,
    	`carno` varchar(10) unique not null,
    	`owner` varchar(20) not null,
    	`brand` varchar(20) not null,
    	primary key (`no`)
    );
    
    create table `tb_record` (
    	`no` integer not null AUTO_INCREMENT,
    	`reason` varchar(200) not null,
    	`punish` varchar(200) not null,
    	`makedate` date not null,
    	`dealt` boolean default 0,
    	`car_id` integer not null,
    	`is_deleted` boolean default 0,
    	`deleted_time` datetime,
    	`updated_time` datetime,
    	primary key (`no`),
    	foreign key (`car_id`) references `tb_car` (`no`)
    );
    
    insert into `tb_car`
    	(`no`, `carno`, `owner`, `brand`)
    values
    	(1,'川A12345','王大锤','QQ'),
    	(2,'川A250SS','白元芳','Benz'),
    	(3,'川B52088','狄仁杰','BMW'),
    	(4,'川A54321','张小虎','Auto'),
    	(5,'川B203T5','张佩琪','RR'),
    	(6,'川AN9980','李乔治','Jetta'),
    	(7,'川A13788','李佩佩','Swift');
    
    insert into `tb_record`
    	(`is_deleted`, `deleted_time`, `updated_time`, `no`, `reason`, `punish`, `makedate`, `dealt`, `car_id`)
    values
    	(0,NULL,NULL,1,'逆向行驶','扣3分,罚款500元','2019-01-01 00:00:00.000000',0,1),
    	(0,NULL,NULL,2,'超速行驶','扣2分,罚款300元','2019-01-10 00:00:00.000000',0,2),
    	(0,NULL,NULL,3,'违章停车','罚款100元','2019-03-15 00:00:00.000000',0,1),
    	(0,NULL,NULL,4,'殴打交警','吊销驾照,拘留15天','2019-04-01 00:00:00.000000',0,1),
    	(0,NULL,NULL,5,'酒驾','扣6分','2019-01-10 00:00:00.000000',0,3),
    	(0,NULL,NULL,6,'醉驾','扣12分,罚款3000元','2019-01-20 00:00:00.000000',0,4),
    	(0,NULL,NULL,7,'逆向行驶','扣3分,罚款500元','2018-12-15 00:00:00.000000',0,1),
    	(0,NULL,NULL,8,'殴打交警','吊销驾照,拘留15天','2019-04-01 00:00:00.000000',0,2),
    	(0,NULL,NULL,9,'酒驾','扣6分','2019-01-10 00:00:00.000000',0,4),
    	(0,NULL,NULL,10,'醉驾','扣12分,罚款3000元','2019-01-20 00:00:00.000000',0,3),
    	(0,NULL,NULL,11,'逆向行驶','扣3分,罚款500元','2018-12-15 00:00:00.000000',0,5),
    	(0,NULL,NULL,12,'逆向行驶','扣3分,罚款500元','2018-07-23 00:00:00.000000',0,7),
    	(0,NULL,NULL,13,'遮挡车牌','扣6分','2019-05-10 00:00:00.000000',0,5),
    	(0,NULL,NULL,14,'超速行驶','扣2分,罚款300元','2018-10-30 00:00:00.000000',0,6);
    
    
  3. 在虚拟环境中安装连接MySQL数据库所需要的依赖项

    pip install mysqlclient -i https://pypi.doubanio.com/simple
    
  4. 修改项目的setting.py文件,首先需要将创建的应用polls添加到已安装的项目INSTALLED_APPS中,然后配置MySQL作为持久化方案。

    INSTALLED_APPS = [
        'django.contrib.admin',
        'django.contrib.auth',
        'django.contrib.contenttypes',
        'django.contrib.sessions',
        'django.contrib.messages',
        'django.contrib.staticfiles',
        'polls'
    ]
    
    DATABASES = {
        'default': {
            # 数据库引擎配置
            'ENGINE': 'django.db.backends.mysql',
            # 数据库名称
            'NAME': 'TPIS',
            # 启动MySQL服务的端口号
            'PORT': '3306',
            # 数据库服务器的IP地址
            'HOST': 'localhost',
            # 数据库用户名
            'USER': 'TPIS',
            # 数据库口令
            'PASSWORD': '[email protected]',
            # 数据库使用的字符集
            'CHARSET': 'utf8',
            # 数据库时间日期的时区设定
            'TIME-ZONE': 'Asia/Shanghai'
        }
    }
    
  5. Django框架提供了ORM来解决数据持久化问题,ORM翻译成中文叫”对象关系映射“。因为Python是面向对象的编程语言,在python中使用对象模型来保存数据,而关系型数据库使用关系模型,用二维表来保存数据,两种模型并不匹配。使用ORM是为了实现对象模型到关系模型的双向转换,这样就不用在Python中书写SQL语句和游标操作,因为遮羞都会由ORM自动完成。利用Django的ORM,可以直接将创建的车辆信息表和违章记录表编程Django中的模型类。

    python manage.py inspectdb > polls/models.py
    

    models.py将会自动生成模型类的代码,对代码进行稍作调整,代码如下所示:

    from django.db import models
    
    
    class Car(models.Model):
        no = models.AutoField(primary_key=True)
        carno = models.CharField(unique=True, max_length=10)
        owner = models.CharField(max_length=20)
        brand = models.CharField(max_length=20)
    
        class Meta:
            managed = False
            db_table = 'tb_car'
    
    
    class Record(models.Model):
        no = models.AutoField(primary_key=True)
        reason = models.CharField(max_length=200)
        punish = models.CharField(max_length=200)
        makedate = models.DateField()
        dealt = models.IntegerField(blank=True, null=True)
        car = models.ForeignKey(to=Car, on_delete=models.DO_NOTHING, db_column='car_id')
        is_deleted = models.IntegerField(blank=True, null=True)
        deleted_time = models.DateTimeField(blank=True, null=True)
        updated_time = models.DateTimeField(blank=True, null=True)
    
        class Meta:
            managed = False
            db_table = 'tb_record'
    
    Make sure each ForeignKey has `on_delete` set to the desired behavior.
    

实现查询首页的效果

  1. 修改polls/views.py文件,编写视图函数实现对查询首页的后端渲染

    视图函数如下:

    def show_index(request: HttpRequest) -> HttpRequest:
        return render(request, 'index.html')
    

    urls.py文件中为视图函数配置路由:

    from polls.views import show_index
    
    urlpatterns = [
        path('admin/', admin.site.urls),
        path('', show_index),
    ]
    

    在path中,不能将show_index误写为show_index(),此处为高阶函数的调用

    配置后运行项目,http://127.0.0.1:8000显示页面如下:

在这里插入图片描述

执行完此步骤,所有的功能都还没有实现,我们需要继续添加视图函数,编写具有查询功能的函数,具体代码如下:

def show_record(request: HttpRequest) -> HttpResponse:
    queryset = Record.objects.all()
    return render(request, 'index.html', {
        "records": queryset
    })

项目执行效果如下:

在这里插入图片描述
此处我们已经完成了点击查询按钮,输入框为空则返回所有违章记录的功能,精确查询和模糊查询的功能还未实现,我们需要继续修改show_record函数

def show_record(request: HttpRequest) -> HttpResponse:
    queryset = Record.objects.all()
    carinfo = request.POST.get('carinfo')
    if carinfo:
        carinfo = re.sub(r'\s', '', carinfo)
        queryset = queryset.filter(
            Q(car__carno__startswith=carinfo) | Q(car__owner__contains=carinfo)
        )
    return render(request, 'index.html', {
        "records": queryset
    })

因为在record表中,car_id是一个外键,通过car__carno外连接到车牌号

car__carno是一个精确查询,使用car__carno__startswith=carinfo可以进行模糊查询

至此,完成了对输入信息的精确查询和模糊查询,我们在此处导入的正则表达式,去掉用户输入信息中包含的空格,增强用户查询时的体验,避免误输入了空格而拿不到查询结果的情况。由于此处的查询需要对不同的对象进行查询,不能用或语句,需要使用Django封装好的Q对象表示两个对象之间表示或者的关系Q(条件一) | Q(条件二)

最后再在show_record函数中添加翻页和分页的代码,这个函数及完成啦!

def show_record(request: HttpRequest) -> HttpResponse:
    page = int(request.GET.get('page', '1'))
    size = int(request.GET.get('size', '5'))
    queryset = Record.objects.filter(is_delete=False)
    carinfo = request.POST.get('carinfo')
    if carinfo:
        carinfo = re.sub(r'\s', '', carinfo)
        queryset = queryset.filter(
            Q(car__carno__istartswith=carinfo) | Q(car__owner__icontains=carinfo)
        )
    count = queryset.count()
    queryset = queryset.order_by('-makedate')[(page - 1) * size:page * size]
    total_page = (count - 1) // size + 1
    return render(request, 'index.html', {
        'records': queryset,
        'current_page': page,
        'page_size': size,
        'total_page': total_page,
        'has_prev': page > 1,
        'has_next': page < total_page,
        'prev_page': page - 1,
        'next_page': page + 1,
        'carinfo': carinfo,

    })

此时需要对index.html稍作调整,保证可以拿到想要的数据结果

<div class="buttons">
            <button data="/search/?page=1&size={{ page_size }}">首页</button>&nbsp;&nbsp;
            {% if has_prev %}<button data="/search/?page={{ prev_page }}&size={{ page_size }}">上一页</button>&nbsp;&nbsp;
            {% else %}<button disabled>上一页</button>&nbsp;&nbsp;
            {% endif %}
            第{{ current_page }}页/共{{ total_page }}页&nbsp;&nbsp;
            {% if has_next %}<button data="/search/?page={{ next_page }}&size={{ page_size }}">下一页</button>&nbsp;&nbsp;
            {% else %}<button disabled>下一页</button>&nbsp;&nbsp;
            {% endif %}
            <button data="/search/?page={{ total_page }}&size={{ page_size }}">末页</button>
        </div>

做完此步骤,我们的分页效果就出来了

实现违章的受理和删除

接下来需要进行受理和删除功能的实现,很明显加入能够在不刷新整个页面的情况下就可以实现这两个功能,会带来更好的用户体验,因此,我们考虑使用Ajax技术来实现”受理“和”删除“。Ajax是Ansynchronous Javascript And XML的缩写,简单的说,使用Ajax技术可以在不重新加载整个页面的情况下对页面经行局部刷新。

对于传统的Web应用,每次页面上需要加载新的内容都需要重新请求服务器并刷新整个页面,如果服务器短时间内无法给予响应或网络状况不理想,那么可能造成浏览器长时间的空白并使得用户处于等待状态,这个期间用户什么都做不了。很显然这样的Web应用并不能带来很好的用户体验。

对于Ajax技术的Web应用,浏览器可以向服务器发起异步请求获取数据。异步请求不会中断用户体验,当服务器返回了新的数据,我们就可以用过JavaScript代码的DOM操作来实现对页面的局部刷新,这样就相当于在不刷新整个页面的情况下更新了页面的内容。

为了实现这个功能,我们需要通过视图函数,通过Django封装的JsonResponse类将字典序列化为JSON字符串作为返回浏览器的响应内容。具体代码如下:

def handle_record(request: HttpRequest) -> HttpResponse:
    try:
        rno = int(request.GET.get('rno'))
        record = Record.objects.filter(no=rno, dealt=False).first()
        if record and not record.dealt:
            record.dealt = True
            record.updated_time = timezone.now()
            record.save()
            data = {'code': 10000, 'message': 'dealt successfully'}
        else:
            data = {'code': 10001, 'message': 'dealt false'}
    except ValueError:
        data = {'code': 10002, 'message': 'invalid car number'}
    return JsonResponse(data)


def delete_record(request: HttpRequest) -> HttpResponse:
    try:
        rno = int(request.GET.get('rno'))
    except ValueError:
        data = {'code': 20002, 'message': 'invalid car number'}
    else:
        record = Record.objects.filter(no=rno, dealt=True).first()
        if record:
            if record.dealt:
                record.is_deleted = True
                record.deleted_time = timezone.now()
                record.save()
                data = {'code': 20000, 'message': 'delete successfully'}
            else:
                data = {'code': 20001, 'message': 'can not delete which is not dealt'}
        else:
            data = {'code': 20003, 'message': 'delete false'}
    return JsonResponse(data)

完成了视图函数,我们需要将函数映射到urls.py文件中,这一步非常重要,不然我们的MVC模型就不能够关联起来。

from polls.views import show_index, show_record, handle_record, delete_record

urlpatterns = [
    path('admin/', admin.site.urls),
    path('', show_index),
    path('search/', show_record),
    path('handle/', handle_record),
    path('delete/', delete_record),
]

此时需要修改index.html的模板页,引入jQuery库来实现事件的处理,Ajax请求和DMO操作

<script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
    <script>
        function loadPageData(evt) {
            $('#search').attr(
                'action', $(evt.target).attr('data')
            ).trigger('submit')
        }

        $(() => {
            $('#export').on('click', (evt) => {
                evt.preventDefault()
                let url = $(evt.target).attr('href')
                let queryParam = '?carinfo=' + $('input[name="carinfo"]').val()
                location.href = url + queryParam
            })

            $('.buttons>button').on('click', loadPageData)
            $('.handle').on('click', (evt) => {
                evt.preventDefault()
                let url = $(evt.target).attr('href')
                $.getJSON(url, (json) => {
                    if (json.code === 10000) {
                        $(evt.target).parent().prev().text('已受理')
                    } else {
                        alert(json.message)
                    }
                })

            })
            $('.delete').on('click', (evt) => {
                evt.preventDefault()
                if (confirm('确定删除该记录吗?')) {
                    let url = $(evt.target).attr('href')
                    $.getJSON(url, (json) => {
                        if (json.code === 20000) {
                            $(evt.target).parent().parent().remove()
                        } else {
                            alert('尚未受理违章信息,不能删除!!!')
                        }
                    })
                }
            })
        })
    </script>

至此,我们整个项目的查询、受理和删除功能已经全部实现。

配置Django-Debug-Toolbar

项目开发阶段,显示足够的调试信息以辅助开发人员调试代码是非常有必要的;项目上线以后,将系统运行时出现的警告、错误等信息记录下来以备相关人员了解系统运行状况并维护代码也是非常有必要的。如果想要调试咱们的Django项目,那么Django-Debug-Toolbar是一个非常不错的选择,它是项目开发阶段调试和优化的必备工具,只要配置了它,就可以很方便查看如下表项目运行的信息,这些信息对调试项目和优化Web项目应用性能都是至关重要的。

项目 说明
Versions Django的版本
Time 显示视图耗费的时间
Settings 配置文件中设置的值
Headers HTTP请求头和响应头的信息
Request 和请求头相关的各种变量及其信息
StaticFiles 静态文件加载情况
Templates 模板的相关信息
Cache 缓存的使用情况
Singals Django内置的信号信息
Logging 被记录的日志信息
SQL 向数据库发送的SQL语句及其执行随时间
  1. 安装Django-Debug-Toolbar

    pip install django-debug-toolbar
    
  2. 配置-修改setting.py文件

    INSTALLED_APPS = [
        'django.contrib.admin',
        'django.contrib.auth',
        'django.contrib.contenttypes',
        'django.contrib.sessions',
        'django.contrib.messages',
        'django.contrib.staticfiles',
        'polls',
        'debug_toolbar', 		# 添加此项
    ]
    
    MIDDLEWARE = [
        'debug_toolbar.middleware.DebugToolbarMiddleware',
        # 添加此项,并且只能放在开头
        'django.middleware.security.SecurityMiddleware',
        'django.contrib.sessions.middleware.SessionMiddleware',
        'django.middleware.common.CommonMiddleware',
        'django.middleware.csrf.CsrfViewMiddleware',
        'django.contrib.auth.middleware.AuthenticationMiddleware',
        'django.contrib.messages.middleware.MessageMiddleware',
        'django.middleware.clickjacking.XFrameOptionsMiddleware',
    ]
    
    # 写入下列语句
    DEBUG_TOOLBAR_CONFIG = {
        # 引入jQuery库
        'JQUERY_URL': 'https://cdn.bootcss.com/jquery/3.3.1/jquery.min.js',
        # 工具栏是否折叠
        'SHOW_COLLAPSED': True,
        # 是否显示工具栏
        'SHOW_TOOLBAR_CALLBACK': lambda x: True,
    }
    
  3. 修改urls.py文件

    from django.conf import settings
    from django.urls import include
    
    if settings.DEBUG:
    
        import debug_toolbar
    
        urlpatterns.insert(0, path('__debug__/', include(debug_toolbar.urls)))
    
  4. 在配置好Django-Debug-Toolbar之后,页面的右侧会看到一个调试工具栏,如下图
    在这里插入图片描述

优化ORM代码

在配置了Django-Debug-Toolbar之后,我们关注的问题是ORM框架生成的SQL查询到底是生么样子的,相信这里的结果会出乎意料,show_record函数执行了queryset = Record.objects.filter(is_delete=False)之后我们看可以注意到,在控制台看到或者通过Django-Debug-Toolbar输出的SQL语句是下面这样的:

SELECT COUNT(*) AS `__count` FROM `tb_record` WHERE `tb_record`.`is_deleted` = 0;
SELECT `tb_record`.`no`,`tb_record`.`reason`,`tb_record`.`punish`,`tb_record`.`makedate`,`tb_record`.`dealt`,`tb_record`.`car_id`,`tb_record`.`is_deleted`,`tb_record`.`deleted_time`,`tb_record`.`updated_time` FROM `tb_record` WHERE `tb_record`.`is_deleted` = 0;
 	
SELECT `tb_car`.`no`,`tb_car`.`carno`,`tb_car`.`owner`,`tb_car`.`brand` FROM `tb_car` WHERE `tb_car`.`no` = 2;
 
SELECT `tb_car`.`no`,`tb_car`.`carno`,`tb_car`.`owner`,`tb_car`.`brand` FROM `tb_car` WHERE `tb_car`.`no` = 1;
 	
SELECT `tb_car`.`no`,`tb_car`.`carno`,`tb_car`.`owner`,`tb_car`.`brand` FROM `tb_car` WHERE `tb_car`.`no` = 4;
 	
SELECT `tb_car`.`no`,`tb_car`.`carno`,`tb_car`.`owner`,`tb_car`.`brand` FROM `tb_car`
WHERE `tb_car`.`no` = 3;
 
SELECT `tb_car`.`no`,`tb_car`.`carno`,`tb_car`.`owner`,`tb_car`.`brand`FROM `tb_car`
WHERE `tb_car`.`no` = 2;

这里的问题通常被称为“1+n”查询,原本获取车辆违章信息的数据只需要一条SQL语句,但是由于车辆违章信息关联了车辆详细信息,当我们查询到N条车辆违章数据时,Django的ORM模型又会向数据库发出N条SQL语句查询车辆详细信息。每条SQL语句执行都会有较大的开销而且会给数据库服务器带来压力,当然如果能够在一条SQL语句中完成车辆信息和违章信息的查询是一个不错的选择,我们这里选择使用连接查询。在Django框架中,对于一对多关联,我们可以使用Queryset中的select_related()方法来加载关联对象;而对于多对多关联,我们可以使用prefetch_related()方法来加载关联对象。

show_record视图函数中,按照如下方法优化代码

queryset = Record.objects.filter(is_deleted=False).select_related('car')

事实上,我们查询违章记录,不需要查询是否已被删除,因为filter()已经将被删除的数据过滤掉,而且删除时间和修改时间我们同样也不需要查询,可以通过queryset的defer()属性指定不需要投影的字段,或者用only()指定需要投影的字段即可。所以我们继续优化代码:

queryset = Record.objects.filter(is_deleted=False)\
        .defer('is_deleted', 'deleted_time', 'updated_time')\
        .select_related('car')

当我们再次运行项目,会发向项目查询的时间相比之前减少了很多,而且这一次的SQL语句仅仅有如下查询

SELECT COUNT(*) AS `__count` FROM `tb_record` WHERE `tb_record`.`is_deleted` = 0;
SELECT `tb_record`.`no`,`tb_record`.`reason`,`tb_record`.`punish`,`tb_record`.`makedate`,`tb_record`.`dealt`,`tb_record`.`car_id`,`tb_car`.`no`,`tb_car`.`carno`,`tb_car`.`owner`,`tb_car`.`brand` FROM `tb_record` INNER JOIN `tb_car` ON (`tb_record`.`car_id` = `tb_car`.`no`)
 WHERE `tb_record`.`is_deleted` = 0 ORDER BY `tb_record`.`makedate` DESC LIMIT 5

制作Excel报表

导出Excel报表:报表就是用表格、图表等格式来动态显示数据,所以有人用这样的公式来描述报表

报表 = 多样式的格式 + 动态数据

有很多的第三方库支持在Python程序中写Excel文件,包括xlwt、xlwings、openpyxl、xlswriter、pandas等,其中xlwt虽然只支持写xls格式的Excel文件,但在性能方面的表现是非常不错的。下面我们就以xlwt为例,来演示如何在Django项目中导出Excel报表。

首先需要安装xlwt第三方库

pip install xlwt -i https://pypi.doubanio.com/simple

然后需要将内容写入Excel工作簿

def export_excel(request: HttpRequest) -> HttpResponse:
    queryset = Record.objects.filter(is_deleted=False)\
        .defer("is_deleted", "deleted_time", "updated_time").select_related('car')
    wb = xlwt.Workbook()
    sheet = wb.add_sheet('违章记录统计表')
    titles = ('编号', '车牌号', '车主姓名', '违章原因', '违章时间', '处罚方式', '是否受理')
    for col_index, title in enumerate(titles):
        sheet.write(0, col_index, title)
    for row_index, record in enumerate(queryset):
        sheet.write(row_index + 1, 0, record.no)
        sheet.write(row_index + 1, 1, record.car.carno)
        sheet.write(row_index + 1, 2, record.car.owner)
        sheet.write(row_index + 1, 3, record.reason)
        sheet.write(row_index + 1, 4, record.makedate)
        sheet.write(row_index + 1, 5, record.punish)
        sheet.write(row_index + 1, 6, '已受理' if record.dealt else '未受理')

写入Excel文件之后,我们到这里还只完成了一半,我们最终的目的是希望用户能够从浏览器将Excel文件下载下来,该怎么做呢?我们这里需要将违章记录的二进制数据交给浏览器, 具体步骤如下:

buffer = io.bytesIO()
wb.save(buffer)
resp = HttpResponse(buffer.getvalue(), content_type='application/vnd.ms-excel')

由于字符串是不可变类型,所以在这里我们不能将字符串直接交给浏览器处理,我们需要通过io.byteIO()将字符串转换成可变的字节串,可以不断地往字节串中写入新的内容,相当于一段新的内存区域。对数据进行保存后,再通过getvalue()属性将字节串变回字符串,将可变字节串变为不可变字节串,这样就可以将字节串交给Httpresponse处理,最后将数据带到浏览器中,这里我们还需要加上Excel的MIME(描述消息内容类型地因特网标准,包括文本、图像、音频)类型。

到这里我们仍然没有做完,我们需要告诉浏览器是需要打开这个数据还是需要将其下载下来。事实上,在没有安装插件的情况下,浏览器不能直接打开Excel文件;对于文件的名字,英文名是完全没有问题的,但是中文名、日文名、韩文名等等,我们需要将其处理为百分号编码。尽管我们这样做了,在Google浏览器中打开没有问题,但是换了其他浏览器,就不一定了,我们还需要将文件名做出utf-8编码的说明

filenanme = quote"违章记录汇总统计表"
resp['content-disposition'] = f'attachment; filename*=utf-8\'\'"{filename}'	
# attachment表示以附件的形式进行下载
# 由于不能直接用单引号 ’ 所以需要做转义处理:\'
return resp

最后将export_excel函数映射到url中,就可以在浏览器中点击下载了,完整的导出报表的代码如下

def export_excel(request: HttpRequest) -> HttpResponse:
    queryset = Record.objects.filter(is_deleted=False)\
        .defer('is_deleted', 'deleted_time', 'updated_time')\
        .select_related('car').order_by('no')
    wb = xlwt.Workbook()
    sheet = wb.add_sheet('违章记录表')
    titles = ('编号', '车牌号', '车主姓名', '违章原因', '违章时间', '处罚方式', '是否受理')
    for col_index, title in enumerate(titles):
        sheet.write(0, col_index, title)
    for row_index, record in enumerate(queryset):
        sheet.write(row_index + 1, 0, record.no)
        sheet.write(row_index + 1, 1, record.car.no)
        sheet.write(row_index + 1, 2, record.car.owner)
        sheet.write(row_index + 1, 3, record.reason)
        sheet.write(row_index + 1, 4, record.makedate.strftime('%Y-%m-%d'))
        sheet.write(row_index + 1, 5, record.punish)
        sheet.write(row_index + 1, 6, '已受理' if record.dealt else '未受理')
    buffer = io.BytesIO()
    wb.save(buffer)
    resp = HttpResponse(buffer.getvalue(), content_type='application/vnd.ms-excel')
    filename = quote('违章记录汇总统计.xls')
    resp['content-disposition'] = f'attachment; filename*=utf-8\'\'{filename}'
    return resp

生成车辆违章记录的条形统计图

这里我们借助https://echarts.apache.org/zh/index.html,可以直接在官方文档中查看教程,做出理想的统计图表,当然图表中的数据,我们都要通过连接服务器发起异步请求,抓取到我们需要的数据。

咱们项目的统计图的X轴需要车辆的车牌号信息,Y轴表示车辆的违章次数。这里首先需要通过SQL语句查询到我们想要的数据才行

select carno, ifnull(total, 0) as total from tb_car t1 left outer join 
(select car_id, count(no) as total from 
 tb_record group by car_id) t2 on t1.no = t2.car_id

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HxhrejTS-1596505597544)(C:\Users\reserve\AppData\Roaming\Typora\typora-user-images\image-20200723190258467.png)]

所以此处我们需要在视图函数中用原生SQL语句查询,

def get_bar_data(request: HttpRequest) -> HttpResponse:
    with connection.cursor() as cursor:
        cursor.execute('select carno, ifnull(total, 0) as total from tb_car t1 '
                       ' left outer join (select car_id, count(no) as total from '
                       ' tb_record group by car_id) t2 on t1.no = t2.car_id')

        xdata, ydata = [], []
        for row in cursor.fetchall():
            xdata.append(row[0])
            ydata.append(row[1])
    return JsonResponse({'xdata': xdata, 'ydata': ydata})

这样我们就分别拿到了横轴和纵轴的数据,将其组装成一个字典通过JsonResponse返回给浏览器(不要忘了视图函数的url映射)

完成了视图函数后,我们需要完成前端页面的Ajax的请求,需要做出如下修改

<script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
<script src="https://cdn.bootcdn.net/ajax/libs/echarts/4.8.0/echarts.simple.js"></script>
    <script type="text/javascript">
        // 基于准备好的dom,初始化echarts实例
        let myChart = echarts.init(document.querySelector('#main'));

        // 指定图表的配置项和数据
        let option = {
            title: {
                text: 'ECharts 入门示例'
            },
            tooltip: {},
            legend: {
                data:['违章次数']
            },
            xAxis: {
                data: []
            },
            yAxis: {},
            series: [{
                name: '违章次数',
                type: 'bar',
                data: []
            }]
        };
        $.getJSON('/bardata/', (json) => {
            option.xAxis.data = json.xdata
            option.series[0].data = json.ydata
            myChart.setOption(option)
        })
        // 使用刚指定的配置项和数据显示图表。
        // myChart.setOption(option);
    </script>

完成了JavaScript代码后,不要忘记在html代码的最后给条形统计图添加一个div,不然没有办法在前端页面显示

<div id="main" style="width: 800px;height:400px;"></div>
<!-- 添加到HTML代码的最后 -->

最后就可以在页面上看到动态生成的统计图了。

在这里插入图片描述

这样,我们整个通过Django后端渲染的车辆违章查询系统就完成了。

详细代码请参考我的码云:

后端渲染:https://gitee.com/dcstempt_ping/carsys_back_end_rendering

猜你喜欢

转载自blog.csdn.net/weixin_45606738/article/details/107779739