当前位置:   article > 正文

【课程设计/毕业设计】python图书借阅管理系统源码+开发文档_图书管理系统的设计与开发代码python

图书管理系统的设计与开发代码python

项目介绍

一直想做一款图书借阅管理系统,看了很多优秀的开源项目但是发现没有合适的。于是利用空闲休息时间开始自己写了一套管理系统。学习过程中遇到问题可以咨询留言。

在线体验

http://book.gitapp.cn/

源码地址

https://github.com/geeeeeeeek/python_book

界面预览

在这里插入图片描述
在这里插入图片描述
图书借阅系统

功能介绍

系统分为后台和前台两部分。

后台的主要功能:

  • 图书管理:管理系统可以录入、修改和查询图书的基本信息,如图书名称、简介、备注等。
  • 类型管理:系统可以管理图书的类型信息,包括类型的名称等。
  • 标签管理:管理标签录入、修改和查询标签的信息。
  • 评论管理:管理和浏览整个网站的评论信息。
  • 借阅管理:管理和浏览整个网站的借阅信息。
  • 用户管理:管理和浏览网站的用户信息,可以新增、编辑和删除用户。
  • 统计分析:系统可以根据图书的活动数据和会员参与度进行统计和分析,帮助管理员了解整个系统的状况。
  • 消息管理:图书管理员可以在系统上发布消息,整个网站的用户都能收到。
  • 系统信息:管理员可以查看系统的基本信息,包括系统名称、服务器信息、内存信息、cpu信息、软件信息等。

前台的主要功能:

  • 注册登录:用户通过注册和登录后,才能使用网站。
  • 门户浏览:用户进入首页后,可以浏览图书列表信息,包括最新、最热、推荐。
  • 智能推荐:详情页右侧的热门推荐。
  • 用户中心:包括用户基本资料修改、用户邮箱推送、消息。
  • 我的借阅:包括我借阅的图书的信息。
  • 模糊搜索:顶部搜索功能,支持模糊搜索图书信息。
  • 图书评论:详情页下侧用户可以评论图书。

开发环境

  • 后端: Python 3.8 + Django 3.2
  • 前端: Javascript + Vue
  • 数据库:MySQL 5.7
  • 开发平台:Pycharm + vscode
  • 运行环境:Windows 10/11

关键技术

  • 前端技术栈 ES6、vue、vuex、vue-router、vue-cli、axios、antd
  • 后端技术栈 Python、Django、pip

后端技术

django框架

Django是一款基于Python开发的全栈式一体化Web应用框架。2003年问世之初,它只是美国一家报社的内部工具,2005年7月使用BSD许可证完成了开源。Django采用MTV设计模式,即Model(模型)+ Template(模板)+ View(视图)。它遵循MVC设计,并且内置了对象关系映射(ORM)层,使得开发者无需关心底层的数据存取细节,可以更专注于业务逻辑的开发。

Django的目的是削减代码量,简单且迅速地搭建以数据库为主体的复杂Web站点。它是全栈式框架,因此安装起来很简单,而且使用者众多。这使得Django除具有完备的官方文档之外,还有大量的关联文档、丰富的第三方库可供使用。与其他框架相比,Django用起来要轻松得多。

优点:

  • 提供了定义序列化器Serializer的方法。可以快速根据Django ORM或者其他库自动序列化或反序列化。
  • 提供了丰富的类视图MIXIN扩展类。可以简化视图的编写。
  • 具有丰富的定制层级。包括函数视图、类视图,还可以将视图与自动生成API结合,满足各种需求。
  • 支持多种身份认证和权限认证方式。
  • 内置了限流系统。

前端技术

  • npm:node.js的包管理工具,用于统一管理我们前端项目中需要用到的包、插件、工具、命令等,便于开发和维护。
  • ES6:Javascript的新版本,ECMAScript6的简称。利用ES6我们可以简化我们的JS代码,同时利用其提供的强大功能来快速实现JS逻辑。
  • vue-cli:Vue的脚手架工具,用于自动生成Vue项目的目录及文件。
  • vue-router: Vue提供的前端路由工具,利用其我们实现页面的路由控制,局部刷新及按需加载,构建单页应用,实现前后端分离。
  • vuex:Vue提供的状态管理工具,用于统一管理我们项目中各种数据的交互和重用,存储我们需要用到数据对象。
  • Ant-design:基于MVVM框架Vue开源出来的一套前端ui组件。

运行步骤

后端运行步骤

(1) 安装mysql数据库,启动服务

(2) 打开cmd命令行,进入mysql,并新建数据库

mysql -u root -p
CREATE DATABASE IF NOT EXISTS python_book DEFAULT CHARSET utf8 COLLATE utf8_general_ci;
  • 1
  • 2

(3) 恢复sql数据

use xxx
source xxxx.sql
  • 1
  • 2

(4) 修改settings.py中的配置信息

(5) 安装python 3.8

(6) 安装依赖包

pip install -r requirements.txt -i https://mirrors.aliyun.com/pypi/simple
  • 1

(7) 运行命令

python manage.py runserver 
  • 1

前端运行步骤

(1) 安装node 16

(2) cmd进入web目录下,安装依赖,执行:

npm install 
  • 1

(3) 运行项目

npm run serve
  • 1

代码结构

后端结构

bookproject  
├── myapp            // 主应用
│       └── auth                     // 认证管理
│       └── middlewares              // 中间件
│       └── permission               // 权限
│       └── views                    // 视图与接口(主要业务代码)
│       └── models.py                // 状态码
│       └── serializers.py           // 状态码
│       └── urls.py                  // 状态码
│       └── utils.py                 // 状态码
├── entity             // 实体类
├── interceptor        // 拦截器
├── mapper             // 数据库映射
├── bookproject        // 配置目录
├── upload             // 静态资源目录
├── requiements.txt    // 依赖项
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

前端结构

├── build                      // 构建相关  
├── public                     // 公共文件
│   ├── favicon.ico            // favicon图标
│   └── index.html             // html模板
├── src                        // 源代码
│   ├── api                    // 所有请求
│   ├── assets                 // 主题 字体等静态资源
│   ├── router                 // 路由
│   ├── store                  // 全局 store管理
│   ├── utils                  // 全局公用方法
│   ├── views                  // view界面
│   ├── App.vue                // 入口页面
│   ├── main.js                // 入口 加载组件 初始化等
│   └── settings.js            // 系统配置
├── .eslintignore              // 忽略语法检查
├── .eslintrc.js               // eslint 配置项
├── .gitignore                 // git 忽略项
├── babel.config.js            // babel.config.js
├── package.json               // package.json
└── vite.config.js             // vue配置
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

数据库设计

需求分析

图书管理系统中,需要存储和管理图书信息、评论信息、分类信息、标签信息、用户信息、通知信息、日志信息。

实体设计如下:

  • 图书(thing)
  • 分类(classification)
  • 标签(tag)
  • 用户(user)
  • 评价(comment)
  • 日志(log)
  • 通知(notice)

关系如下:

  • 一个图书有一个分类
  • 一个分类可以对应多个图书
  • 一个图书有多个标签
  • 一个标签可以对应多个图书

数据表设计如下:

// 图书表
Table book {
    book_id int [pk]
    classification_id int [ref: > C.classification_id]
    tag_id int [ref: <> tag.tag_id]
    title varchar
    original_title varchar
    cover varchar
    author varchar
    translator varchar
    press varchar
    page_count int
    price varchar
    isbn varchar
    pub_date varchar
    online_time varchar
    status int
    repertory int
    score varchar
    layout varchar
    description text
    create_time datetime
    pv int
    wish_count int
    recommend_count int
    wish int [ref: <> user.user_id]
    collect int [ref: <> user.user_id]
 }
 // 图书分类
 Table classification as C {
   classification_id int [pk]
   pid int
   title varchar
   create_time datetime
 }
 // 标签
 Table tag {
   tag_id int [pk]
   title varchar
   create_time datetime
 }
 // 评论表
 Table comment {
   comment_id int [pk]
   content varchar
   user_id int [ref: > user.user_id]
   book_id int [ref: > book.book_id]
   comment_time datetime
   like_count int
 }
 // 用户表
 Table user {
   user_id int [pk]
   role varchar // 1管理员 2普通用户 3演示帐号
   status int // 0正常 1封号
   username varchar
   password varchar
   nickname varchar
   avatar varchar
   description varchar
   wish int [ref: <> book.book_id]
   email varchar
   mobile varchar
   score int // 积分
   push_email varchar // 推送邮箱
   push_switch int  // 推送开关
   token varchar
   admin_token varchar
 }
 
 
 // 登录日志
 Table login_log {
   log_id int [pk]
   username varchar
   ip varchar
   log_time datetime
 }
 
 // 操作日志
 Table op_log {
   id int [pk]
   re_ip varchar
   re_time datetime
   re_url varchar
   re_method varchar
   re_content varchar
   access_time varchar
 }
 
 // 异常日志
 Table error_log {
   id int [pk]
   ip varchar
   method varchar
   content varchar
   log_time varchar
 }
 // 借阅表
 Table borrow {
   borrow_id int [pk]
   user_id int [ref: > user.user_id]
   book_id int [ref: > book.book_id]
   status varchar // 1借出 2已还
   borrow_time datetime
   expect_time datetime // 应还时间
   delayed boolean // 是否点击延期了 1已延期
 }
 
 
 
 
 // 通知
 Table notice {
   id int [pk]
   content varchar
   create_time datetime
 }
 
 
  Table address {
   address_id int [pk]
   user_id int [ref: > user.user_id]
   address_content varchar
   default int  // 是否默认地址
   create_time datetime
 }
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125
  • 126
  • 127
  • 128

开发过程

无论是图书管理、用户管理、标签管理、分类管理、评价管理、日志管理、消息管理等功能都是基于springboot+vue框架开发的,开发流程是:

  • 第一步:编写实体
  • 第二步:编写序列化层
  • 第三步:编写views层
  • 第四步:编写界面和API

下面用图书管理功能来演绎这个流程,其它的管理功能都是这个流程。

第一步:编写实体类

在bookproject下的myapp下的models.py下面新建Thing类。并写入如下代码:

class Thing(models.Model):
    STATUS_CHOICES = (
        ('0', '上架'),
        ('1', '下架'),
    )
    id = models.BigAutoField(primary_key=True)
    classification = models.ForeignKey(Classification, on_delete=models.CASCADE, blank=True, null=True,
                                       related_name='classification_thing')
    tag = models.ManyToManyField(Tag, blank=True)
    title = models.CharField(max_length=100, blank=True, null=True)
    cover = models.ImageField(upload_to='cover/', null=True)
    description = models.TextField(max_length=1000, blank=True, null=True)
    price = models.CharField(max_length=50, blank=True, null=True) 
    mobile = models.CharField(max_length=50, blank=True, null=True)
    age = models.CharField(max_length=10, blank=True, null=True) 
    location = models.CharField(max_length=50, blank=True, null=True) 
    status = models.CharField(max_length=1, choices=STATUS_CHOICES, default='0')
    create_time = models.DateTimeField(auto_now_add=True, null=True)
    pv = models.IntegerField(default=0)
    recommend_count = models.IntegerField(default=0)
    wish = models.ManyToManyField(User, blank=True, related_name="wish_things")
    wish_count = models.IntegerField(default=0)
    collect = models.ManyToManyField(User, blank=True, related_name="collect_things")
    collect_count = models.IntegerField(default=0)

    class Meta:
            db_table = "b_thing"
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27

第二步:编写序列化层

在bookproject下的myapp下的serializers.py下新建ThingSerializer类,并写入代码:

class ThingSerializer(serializers.ModelSerializer):
    # 额外字段
    classification_title = serializers.ReadOnlyField(source='classification.title')

    class Meta:
        model = Thing
        fields = '__all__'

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

第三步:编写views层

在bookproject的myapp下的views下,新建Thing.py代码,并写入代码,实现增删改查


# 查
@api_view(['GET'])
def list_api(request):
    if request.method == 'GET':
        keyword = request.GET.get("keyword", None)
        c = request.GET.get("c", None)
        tag = request.GET.get("tag", None)
        if keyword:
            things = Thing.objects.filter(title__contains=keyword).order_by('create_time')
        elif c:
            classification = Classification.objects.get(pk=c)
            things = classification.classification_thing.all()
        elif tag:
            tag = Tag.objects.get(id=tag)
            print(tag)
            things = tag.thing_set.all()
        else:
            things = Thing.objects.all().order_by('create_time')

        serializer = ThingSerializer(things, many=True)
        return APIResponse(code=0, msg='查询成功', data=serializer.data)

# 删
@api_view(['GET'])
def detail(request):

    try:
        pk = request.GET.get('id', -1)
        thing = Thing.objects.get(pk=pk)
    except Thing.DoesNotExist:
        utils.log_error(request, '对象不存在')
        return APIResponse(code=1, msg='对象不存在')

    if request.method == 'GET':
        serializer = ThingSerializer(thing)
        return APIResponse(code=0, msg='查询成功', data=serializer.data)

# 增
@api_view(['POST'])
@authentication_classes([AdminTokenAuthtication])
def create(request):

    if isDemoAdminUser(request):
        return APIResponse(code=1, msg='演示帐号无法操作')

    serializer = ThingSerializer(data=request.data)
    if serializer.is_valid():
        serializer.save()
        return APIResponse(code=0, msg='创建成功', data=serializer.data)
    else:
        print(serializer.errors)
        utils.log_error(request, '参数错误')

    return APIResponse(code=1, msg='创建失败')

# 改
@api_view(['POST'])
@authentication_classes([AdminTokenAuthtication])
def update(request):

    if isDemoAdminUser(request):
        return APIResponse(code=1, msg='演示帐号无法操作')

    try:
        pk = request.GET.get('id', -1)
        thing = Thing.objects.get(pk=pk)
    except Thing.DoesNotExist:
        return APIResponse(code=1, msg='对象不存在')

    serializer = UpdateThingSerializer(thing, data=request.data)
    if serializer.is_valid():
        serializer.save()
        return APIResponse(code=0, msg='查询成功', data=serializer.data)
    else:
        print(serializer.errors)
        utils.log_error(request, '参数错误')

    return APIResponse(code=1, msg='更新失败')

# 删
@api_view(['POST'])
@authentication_classes([AdminTokenAuthtication])
def delete(request):

    if isDemoAdminUser(request):
        return APIResponse(code=1, msg='演示帐号无法操作')

    try:
        ids = request.GET.get('ids')
        ids_arr = ids.split(',')
        Thing.objects.filter(id__in=ids_arr).delete()
    except Thing.DoesNotExist:
        return APIResponse(code=1, msg='对象不存在')
    return APIResponse(code=0, msg='删除成功')


  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97

然后将该接口添加到urls.py中即可。

第四步:编写界面和API

打开前端web工程,在views文件夹下新建book.vue文件,并编写代码:

<template>
  <div class="page-view">
    <div class="table-operation">
      <a-space>
        <a-button type="primary" @click="handleAdd">新增</a-button>
        <a-button @click="handleBatchDelete">批量删除</a-button>
        <a-input-search addon-before="书名" enter-button @search="onSearch" @change="onSearchChange" />
      </a-space>
    </div>
    <div class="table-wrap" ref="tableWrap">
      <a-table
        size="middle"
        rowKey="id"
        :loading="loading"
        :columns="columns"
        :data-source="data"
        :scroll="{ x: 'max-content' }"
        :row-selection="rowSelection()"
        :pagination="{
          size: 'default',
          current: page,
          pageSize: pageSize,
          onChange: (current) => page = current,
          pageSizeOptions: ['10', '20', '30', '40'],
          showSizeChanger: true,
          showTotal: (total) => `共${total}条数据`
        }"
      >
<!--        <span slot="cover" slot-scope="text">-->
<!--          <img :src="text" style="width: 40px;height: 60px;"/>-->
<!--        </span>-->
        <span slot="operation" class="operation" slot-scope="text, record">
          <a-space :size="16">
            <a @click="handleEdit(record)">编辑</a>
            <a @click="handleDelete(record)">删除</a>
          </a-space>
        </span>
      </a-table>
    </div>
  </div>
</template>

<script>
import {listApi, deleteApi, createApi} from '@/api/admin/book'
import EditBook from '@/views/admin/model/edit-book'

const columns = [
  {
    title: '序号',
    dataIndex: 'index',
    key: 'index',
    width: 60
  },
  {
    title: '书名',
    dataIndex: 'title',
    key: 'title'
  },
  {
    title: '原书名',
    dataIndex: 'original_title',
    key: 'original_title'
  },
  // {
  //   title: '封面',
  //   dataIndex: 'cover',
  //   key: 'cover',
  //   scopedSlots: { customRender: 'cover' }
  // },
  {
    title: '作者',
    dataIndex: 'author',
    key: 'author'
  },
  {
    title: '译者',
    dataIndex: 'translator',
    key: 'translator'
  },
  {
    title: '出版社',
    dataIndex: 'press',
    key: 'press'
  },
  {
    title: '页数',
    dataIndex: 'page_count',
    key: 'page_count'
  },
  {
    title: '价格',
    dataIndex: 'price',
    key: 'price'
  },
  {
    title: 'ISBN',
    dataIndex: 'isbn',
    key: 'isbn'
  },
  {
    title: '出版日期',
    dataIndex: 'pub_date',
    key: 'pub_date'
  },
  {
    title: '状态',
    dataIndex: 'status',
    key: 'status',
    customRender: (text) => text === '0' ? '上架' : '下架'
  },
  {
    title: '库存',
    dataIndex: 'repertory',
    key: 'repertory'
  },
  {
    title: '装饰',
    dataIndex: 'layout',
    key: 'layout'
  },
  {
    title: '简介',
    dataIndex: 'description',
    key: 'description',
    customRender: (text) => text ? text.substring(0, 5) + '...' : '--'
  },
  {
    title: '操作',
    dataIndex: 'action',
    align: 'center',
    fixed: 'right',
    width: 140,
    scopedSlots: { customRender: 'operation' }
  }
]

export default {
  name: 'Book',
  data () {
    return {
      loading: false,
      selectedRowKeys: [],
      columns,
      data: [],
      keyword: undefined,
      pageSize: 10,
      page: 1
    }
  },
  methods: {
    getList () {
      this.loading = true
      listApi({
        keyword: this.keyword
      }).then(res => {
        this.loading = false
        res.data.forEach((item, index) => {
          item.index = index + 1
          if (item.cover) {
            item.cover = this.$BASE_URL + item.cover
          }
        })
        this.data = res.data
        console.log(res)
      }).catch(err => {
        this.$message.error(err.msg)
        this.loading = false
      })
    },
    onSearchChange (e) {
      this.keyword = e.target.value
    },
    onSearch (value) {
      this.getList()
    },
    rowSelection () {
      return {
        onChange: (selectedRowKeys, selectedRows) => {
          this.selectedRowKeys = selectedRowKeys
        }
      }
    },
    // async handleMockAdd () {
    //    // 模拟新增
    //   for (let i = 0; i < 30; i++) {
    //     const desc = '他既到了北京、上海等繁华都市,也抵达了黑龙江朗乡、新疆吐鲁番等疆界边陲。他走到中国大地上,与各种各样的人聊天,在上海人民公园英语角里练习口语的年轻人、想去做进出口生意的学生、刚刚经历过浩劫的知识分子……他用犀利幽默的口吻,一路吐槽不断,但也用冷静、理智的眼光,剖析中国人的性格,发现时代变迁下中国人的生活日常与思想变化。'
    //     const formData = new FormData()
    //     formData.append('title', '生活图书' + i)
    //     formData.append('description', desc)
    //     formData.append('author', '马丽丽'+i)
    //     formData.append('translator', '')
    //     formData.append('isbn', '97875327617' + i)
    //     formData.append('price', Math.floor(Math.random() * 100).toString())
    //     formData.append('press', '现代出版社')
    //     formData.append('layout', '平装')
    //     formData.append('page_count', Math.floor(Math.random() * 500).toString())
    //     formData.append('repertory', Math.floor(Math.random() * 100).toString())
    //     formData.append('status', '0')
    //     formData.append('pub_date', '2011-09-28')
    //
    //     await createApi(formData)
    //     console.log('finish '+i)
    //   }
    // },
    handleAdd () {
      this.$dialog(
        EditBook,
        {
          on: {
            ok: () => {
              this.page = 1
              this.getList()
            }
          }
        },
        {
          title: '新增图书',
          width: '640px',
          centered: true,
          bodyStyle: {
            maxHeight: 'calc(100vh - 200px)',
            overflowY: 'auto'
          }
        }
      )
    },
    handleEdit (record) {
      this.$dialog(
        EditBook,
        {
          book: Object.assign({}, record),
          modifyFlag: true,
          on: {
            ok: () => {
              this.getList()
            }
          }
        },
        {
          title: '编辑图书',
          width: '640px',
          centered: true,
          bodyStyle: {
            maxHeight: 'calc(100vh - 200px)',
            overflowY: 'auto'
          }
        }
      )
    },
    // 删除
    handleDelete (record) {
      const that = this
      this.$confirm({
        title: '确定删除?',
        onOk () {
          deleteApi({
            ids: record.id
          }).then(res => {
            that.$message.success('删除成功')
            that.getList()
          }).catch(err => {
            that.$message.error(err.msg || '删除失败')
          })
        }
      })
    },
    // 批量删除
    handleBatchDelete () {
      console.log(this.selectedRowKeys)
      if (this.selectedRowKeys.length <= 0) {
        this.$message.warn('请勾选删除项')
        return
      }
      const that = this
      this.$confirm({
        title: '确定删除?',
        onOk () {
          deleteApi({
            ids: that.selectedRowKeys.join(',')
          }).then(res => {
            that.$message.success('删除成功')
            that.selectedRowKeys = []
            that.getList()
          }).catch(err => {
            that.$message.error(err.msg || '删除失败')
          })
        }
      })
    }
  },
  mounted () {
    this.getList()
  }
}
</script>

<style lang="less" scoped>
.table-wrap {
  flex: 1;
}
.page-view {
  min-height: 100%;
  background: #FFF;
  padding: 24px;
  display: flex;
  flex-direction: column;
}
.table-operation {
  height: 50px;
  text-align: right;
}
</style>

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125
  • 126
  • 127
  • 128
  • 129
  • 130
  • 131
  • 132
  • 133
  • 134
  • 135
  • 136
  • 137
  • 138
  • 139
  • 140
  • 141
  • 142
  • 143
  • 144
  • 145
  • 146
  • 147
  • 148
  • 149
  • 150
  • 151
  • 152
  • 153
  • 154
  • 155
  • 156
  • 157
  • 158
  • 159
  • 160
  • 161
  • 162
  • 163
  • 164
  • 165
  • 166
  • 167
  • 168
  • 169
  • 170
  • 171
  • 172
  • 173
  • 174
  • 175
  • 176
  • 177
  • 178
  • 179
  • 180
  • 181
  • 182
  • 183
  • 184
  • 185
  • 186
  • 187
  • 188
  • 189
  • 190
  • 191
  • 192
  • 193
  • 194
  • 195
  • 196
  • 197
  • 198
  • 199
  • 200
  • 201
  • 202
  • 203
  • 204
  • 205
  • 206
  • 207
  • 208
  • 209
  • 210
  • 211
  • 212
  • 213
  • 214
  • 215
  • 216
  • 217
  • 218
  • 219
  • 220
  • 221
  • 222
  • 223
  • 224
  • 225
  • 226
  • 227
  • 228
  • 229
  • 230
  • 231
  • 232
  • 233
  • 234
  • 235
  • 236
  • 237
  • 238
  • 239
  • 240
  • 241
  • 242
  • 243
  • 244
  • 245
  • 246
  • 247
  • 248
  • 249
  • 250
  • 251
  • 252
  • 253
  • 254
  • 255
  • 256
  • 257
  • 258
  • 259
  • 260
  • 261
  • 262
  • 263
  • 264
  • 265
  • 266
  • 267
  • 268
  • 269
  • 270
  • 271
  • 272
  • 273
  • 274
  • 275
  • 276
  • 277
  • 278
  • 279
  • 280
  • 281
  • 282
  • 283
  • 284
  • 285
  • 286
  • 287
  • 288
  • 289
  • 290
  • 291
  • 292
  • 293
  • 294
  • 295
  • 296
  • 297
  • 298
  • 299
  • 300
  • 301
  • 302
  • 303
  • 304
  • 305
  • 306
  • 307
  • 308
  • 309
  • 310
  • 311
  • 312
  • 313

这就是图书管理功能的实现流程,其它的功能管理实现一模一样的。按照这个流程编写即可。

重要模块实现

分页实现

基于ant-design框架的a-table的分页插件。

// 分页变量

  const data = reactive({
    dataList: [],
    loading: false,
    keyword: '',
    selectedRowKeys: [] as any[],
    pageSize: 10,
    page: 1,
  });
  
// 分页插件
:pagination="{
          size: 'default',
          current: data.page,
          pageSize: data.pageSize,
          onChange: (current) => (data.page = current),
          showSizeChanger: false,
          showTotal: (total) => `共${total}条数据`,
        }"

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

请求工具实现

前端的请求工具是基于axios开发的,位于utils的requests.js中。封装了request请求和拦截器。

import axios from 'axios'
import store from '@/store'
import storage from 'store'
import notification from 'ant-design-vue/es/notification'
import {ADMIN_TOKEN, BASE_URL, TOKEN} from '@/store/constants'

// 创建 axios 实例
const request = axios.create({
  baseURL: BASE_URL,
  timeout: 180000
})

// 异常拦截处理器
const errorHandler = (error) => {
  if (error.response) {
    const data = error.response.data
    console.log(data)
    // 从 localstorage 获取 token
    if (error.response.status === 403) {
      notification.error({
        message: '未登录',
        description: '登录验证失败'
      })

      // 后台认证失败
      if (data.detail === 'AUTH_FAIL_END') {
        store.dispatch('AdminLogout').then(() => {
          setTimeout(() => {
            window.location.reload()
          }, 500)
        })
      }

      // 前台认证失败
      if (data.detail === 'AUTH_FAIL_FRONT') {
        store.dispatch('Logout').then(() => {
          setTimeout(() => {
            window.location.reload()
          }, 500)
        })
      }
    }
  }
  return Promise.reject(error)
}

// request interceptor
request.interceptors.request.use(config => {
  const adminToken = storage.get(ADMIN_TOKEN)
  const token = storage.get(TOKEN)

  config.headers['Access-Control-Allow-Headers'] = 'adminToken, token,Content-Type'

  // config.headers['Authorization'] = ''
  config.headers['ADMINTOKEN'] = adminToken
  config.headers['TOKEN'] = token
  return config
}, errorHandler)

// response interceptor
request.interceptors.response.use((response) => {
  if (response.data.code !== 200 && response.data.code !== 0) {
    return Promise.reject(response.data)
  } else {
    return response.data
  }
}, errorHandler)

export default request


  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71

权限控制模块

权限控制使用了BaseAuthentication实现的,具体代码可参考authentication.py

from rest_framework import exceptions
from rest_framework.authentication import BaseAuthentication
from myapp.models import User

# 接口认证
class AdminTokenAuthtication(BaseAuthentication):
    def authenticate(self, request):
        adminToken = request.META.get("HTTP_ADMINTOKEN")

        print("检查adminToken==>" + adminToken)
        users = User.objects.filter(admin_token=adminToken)
        """
        判定条件:
            1. 传了adminToken 
            2. 查到了该帐号 
            3. 该帐号是管理员或演示帐号
        """
        if not adminToken or len(users) == 0 or users[0].role == '2':
            raise exceptions.AuthenticationFailed("AUTH_FAIL_END")
        else:
            print('adminToken验证通过')


  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23

路由模块实现

前端的路由是基于vue-router框架实现的,路由文件位于src的rooter的index.js文件中。预览如下:

const constantRouterMap = [
  // ************* 前台路由 **************
  {
    path: '/',
    redirect: '/index'
  },
  {
    path: '/index',
    name: 'index',
    redirect: '/index/portal',
    component: () => import('@/views/index'),
    children: [
      {
        path: 'login',
        name: 'login',
        component: () => import('@/views/index/login')
      },
      {
        path: 'register',
        name: 'register',
        component: () => import('@/views/index/register')
      },
      {
        path: 'portal',
        name: 'portal',
        component: () => import('@/views/index/portal')
      },
      {
        path: 'detail',
        name: 'detail',
        component: () => import('@/views/index/detail')
      },
      {
        path: 'search',
        name: 'search',
        component: () => import('@/views/index/search')
      },
      {
        path: 'user',
        name: 'user',
        redirect: 'user/addressView',
        component: () => import('@/views/index/user'),
        children: [
          {
            path: 'addressView',
            name: 'addressView',
            component: () => import('@/views/index/user/address-view')
          },
          {
            path: 'wishBookView',
            name: 'wishBookView',
            component: () => import('@/views/index/user/wish-book-view')
          },
          {
            path: 'collectBookView',
            name: 'collectBookView',
            component: () => import('@/views/index/user/collect-book-view')
          },
          {
            path: 'orderView',
            name: 'orderView',
            component: () => import('@/views/index/user/order-view')
          },
          {
            path: 'borrowView',
            name: 'borrowView',
            component: () => import('@/views/index/user/borrow-view')
          },
          {
            path: 'userInfoEditView',
            name: 'userInfoEditView',
            component: () => import('@/views/index/user/userinfo-edit-view')
          },
          {
            path: 'followView',
            name: 'followView',
            component: () => import('@/views/index/user/follow-view')
          },
          {
            path: 'fansView',
            name: 'fansView',
            component: () => import('@/views/index/user/fans-view')
          },
          {
            path: 'scoreView',
            name: 'scoreView',
            component: () => import('@/views/index/user/score-view')
          },
          {
            path: 'commentView',
            name: 'commentView',
            component: () => import('@/views/index/user/comment-view')
          },
          {
            path: 'securityView',
            name: 'securityView',
            component: () => import('@/views/index/user/security-view')
          },
          {
            path: 'pushView',
            name: 'pushView',
            component: () => import('@/views/index/user/push-view')
          },
          {
            path: 'messageView',
            name: 'messageView',
            component: () => import('@/views/index/user/message-view')
          },
        ]
      }
    ]
  },
  // ************* 后台路由 **************
  {
    path: '/admin',
    name: 'admin',
    redirect: '/admin/overview',
    component: () => import('@/views/admin/layout/adminLayout'),
    children: [
      {
        path: 'overview',
        name: 'overview',
        component: () => import('@/views/admin/overview')
      },
      {
        path: 'book',
        name: 'book',
        component: () => import('@/views/admin/book')
      },
      {
        path: 'classification',
        name: 'classification',
        component: () => import('@/views/admin/classification')
      },
      {
        path: 'tag',
        name: 'tag',
        component: () => import('@/views/admin/tag')
      },
      {
        path: 'loginLog',
        name: 'loginLog',
        component: () => import('@/views/admin/login-log')
      },
      {
        path: 'opLog',
        name: 'opLog',
        component: () => import('@/views/admin/op-log')
      },
      {
        path: 'errorLog',
        name: 'errorLog',
        component: () => import('@/views/admin/error-log')
      },
      {
        path: 'banner',
        name: 'banner',
        component: () => import('@/views/admin/banner')
      },
      {
        path: 'ad',
        name: 'ad',
        component: () => import('@/views/admin/ad')
      },
      {
        path: 'notice',
        name: 'notice',
        component: () => import('@/views/admin/notice')
      },
      {
        path: 'comment',
        name: 'comment',
        component: () => import('@/views/admin/comment')
      },
      {
        path: 'borrow',
        name: 'borrow',
        component: () => import('@/views/admin/borrow')
      },
      {
        path: 'user',
        name: 'user',
        component: () => import('@/views/admin/user')
      },
      {
        path: 'sysInfo',
        name: 'sysInfo',
        component: () => import('@/views/admin/sys-info')
      }
    ]
  },
  {
    path: '/admin-login',
    name: 'admin-login',
    component: () => import('@/views/admin/admin-login')
  }
]

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125
  • 126
  • 127
  • 128
  • 129
  • 130
  • 131
  • 132
  • 133
  • 134
  • 135
  • 136
  • 137
  • 138
  • 139
  • 140
  • 141
  • 142
  • 143
  • 144
  • 145
  • 146
  • 147
  • 148
  • 149
  • 150
  • 151
  • 152
  • 153
  • 154
  • 155
  • 156
  • 157
  • 158
  • 159
  • 160
  • 161
  • 162
  • 163
  • 164
  • 165
  • 166
  • 167
  • 168
  • 169
  • 170
  • 171
  • 172
  • 173
  • 174
  • 175
  • 176
  • 177
  • 178
  • 179
  • 180
  • 181
  • 182
  • 183
  • 184
  • 185
  • 186
  • 187
  • 188
  • 189
  • 190
  • 191
  • 192
  • 193
  • 194
  • 195
  • 196
  • 197
  • 198

限速功能实现

限流(Throttle)就是限制客户端对API 的调用频率,是API开发者必须要考虑的因素。比如个别客户端(比如爬虫程序)短时间发起大量请求,超过了服务器能够处理的能力,将会影响其它用户的正常使用。又或者某个接口占用数据库资源比较多,如果同一时间该接口被大量调用,服务器可能会陷入僵死状态。为了保证API服务的稳定性,并防止接口受到恶意用户的攻击,我们必须要对我们的API服务进行限流。

我们使用了django的AnonRateThrottle限流类来实现的。可以参见myapp的auth目录下的MyRateThrottle.py文件

class MyRateThrottle(AnonRateThrottle):
    THROTTLE_RATES = {"anon": "2/min"}  # 限流每分钟只能请求2次
  • 1
  • 2

当某个api接口需要限流的时候,只需要添加注解即可,如下所示

@api_view(['POST'])
@throttle_classes([MyRateThrottle]) # 限流注解
def create(request):
    serializer = CommentSerializer(data=request.data)
    if serializer.is_valid():
        serializer.save()
        return APIResponse(code=0, msg='创建成功', data=serializer.data)
    else:
        print(serializer.errors)

    return APIResponse(code=1, msg='创建失败')
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

常见问题

  • 数据库版本有要求吗?

需要mysql 5.7以上

  • 前端 npm install 失败怎么办?

使用国内镜像安装,设置命令为:

npm config set registry https://registry.npm.taobao.org
  • 1
  • 提示"演示账号无法操作",怎么办?

将用户的权限提高,修改b_user表的role字段

  • 如何更换后端请求地址

修改store文件夹下的constants.js文件中的BASE_URL,改成你自己的后端地址

  • 如何新增页面

在views文件夹下创建新的vue文件,写入界面代码,然后在router的root.js中添加路由即可。

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/小蓝xlanll/article/detail/555990
推荐阅读
相关标签
  

闽ICP备14008679号