最全最细的订单交易系统复盘-cnblog

最全最细的订单交易系统复盘

0.项目技术栈

项目采用前后端不分离的模式开发

  • 后端:python的django框架
  • 前端:借用django里面的模板层语法、
  • 数据库:mysql
  • 缓存:redis

1.项目环境搭建

1.1 创建django项目

  1. 创建纯python项目虚拟环境

image-20230711110310636

  1. pycharm终端安装django3.2版本
pip install django==3.2
  1. 终端执行创建django项目命令
django-admin startproject order_transaction_system2 .

注意:

  1. 创建django项目命令后面加个. 表示创建django项目文件夹的路径是在当前执行命令的目录
  2. django项目的文件名必须和刚刚上面创建的python项目名同名,否则报错,这是因为后面加了个点的原因
  3. 如果不加. django项目的文件名可以随意
  1. 启动django服务
# pycharm终端执行以下命令
python manage.py runserver
# 这个命令后面还可以指定运行的ip和端口,详细用法,可以百度!

image-20230711112847903

上述启动方法,每次启动太麻烦,我们可以在pycharm中配置快速启动快捷键

  1. 配置pycharm中快速启动django的快捷键

image-20230711113155813

image-20230711121744210

image-20230711121942477

image-20230711122018104

1.2 创建app

  1. 终端执行创建app命令

    python manage.py startapp web
    
  2. settings注册该app

    image-20230711145938901

    注:使用app之前一定要先注册app,否则会报错

1.3 修改settings里的一些配置

  1. 注释掉,我们项目不需要用到的app和中间件,这样可以减轻django的"重量"

    image-20230711123215966

    image-20230711123253384

    image-20230711123333991

    image-20230711123423837

  2. 配置项目需要用到的静态文件资源存放路径

    image-20230711131908225

  3. 配置我们需要连接的mysql数据库

    image-20230711132256734

    image-20230711132525318

  4. settings里配置redis缓存

    • 先安装django-redis模块

      pip install django-redis
      
    • settings里进行配置

      # redis的相关配置
      CACHES = {
          "default": {
              "BACKEND": "django_redis.cache.RedisCache",
              "LOCATION": "redis://127.0.0.1:6379", # ip&端口信息
              "OPTIONS": {
                  "CLIENT_CLASS": "django_redis.client.DefaultClient",
                  "CONNECTION_POOL_KWARGS": {"max_connections": 100} # 缓存池最大100个连接
                  # "PASSWORD": "密码",
              }
          }
      }
      
  5. settings里配置session存储

    INSTALLED_APPS = [
        # 'django.contrib.admin',
        # 'django.contrib.auth',
        # 'django.contrib.contenttypes',
        # 'django.contrib.sessions',
        # 'django.contrib.messages',
        'django.contrib.staticfiles',
        "app01.apps.App01Config",
    ]
    
    MIDDLEWARE = [
        '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',
    ]
    
    
    # django原本默认是将session存储到session数据表里的,我们不需要存储到数据库里可以直接注释掉相关配置,然后在下面配置我们希望session存储到redis缓存中
    
    # session
    SESSION_ENGINE = 'django.contrib.sessions.backends.cache'
    SESSION_CACHE_ALIAS = 'default' 
    
    SESSION_COOKIE_NAME = "sid"  # Session的cookie保存在浏览器上时的key,即:sessionid=随机字符串
    SESSION_COOKIE_PATH = "/"  # Session的cookie保存的路径
    SESSION_COOKIE_DOMAIN = None  # Session的cookie保存的域名
    SESSION_COOKIE_SECURE = False  # 是否Https传输cookie
    SESSION_COOKIE_HTTPONLY = True  # 是否Session的cookie只支持http传输
    SESSION_COOKIE_AGE = 1209600  # Session的cookie失效日期(2周)
    
    SESSION_EXPIRE_AT_BROWSER_CLOSE = False  # 是否关闭浏览器使得Session过期
    SESSION_SAVE_EVERY_REQUEST = True  # 是否每次请求都保存Session,默认修改之后才保存
    

2.项目所需要的数据表创建与迁移

一个项目的开始,最重要的就是数据表的设计,这个项目需要用到以下几张表:

  • 管理员信息表
  • 客户信息表
  • 客户等级折扣表
  • 价格策略表
  • 客户订单表
  • 客户交易记录表

2.1 在app里面的model.py定义表结构

from django.db import models

# Create your models here.


class BasicModel(models.Model):
    """公共字段虚拟表:对于很多表都共有的字段,我们可以放在一张表里,需要用到这些字段的时候,直接继承这个虚拟表就行"""
    active = models.SmallIntegerField(verbose_name='状态', choices=((1, '激活'), (0, '删除')), default=1)
    create_date = models.DateTimeField(verbose_name='创建日期', auto_now_add=True)

    # 在类中定义一个这个类,写上abstract = True,这样在数据库迁移时就不会创建这张表了
    class Meta:
        abstract = True


class Administrator(BasicModel):
    """管理员表"""
    username = models.CharField(verbose_name='用户名',max_length=16,db_index=True)
    password = models.CharField(verbose_name='密码',max_length=64)
    mobile = models.CharField(verbose_name='手机号',max_length=11,db_index=True)


class Level(BasicModel):
    """客户等级折扣表"""
    title = models.CharField(verbose_name='级别',max_length=12)
    percent = models.SmallIntegerField(verbose_name='折扣')


class Customer(BasicModel):
    """客户表"""
    username = models.CharField(verbose_name='用户名',max_length=16,db_index=True)
    password = models.CharField(verbose_name='密码',max_length=64)
    mobile = models.CharField(verbose_name='手机号',max_length=11,db_index=True)
    balance = models.DecimalField(verbose_name='账户余额',max_digits=10,decimal_places=2,default=0)
    level = models.ForeignKey(verbose_name='级别',to='Level',on_delete=models.CASCADE)
    creator = models.ForeignKey(verbose_name='创建者',to='Administrator',on_delete=models.CASCADE)


class PricePolicy(BasicModel):
    """价格策略表
        1  1000 10
        2  2000 18
    """
    count = models.IntegerField(verbose_name='数量')
    price = models.DecimalField(verbose_name='价格',max_digits=10,decimal_places=2)


class Order(BasicModel):
    """订单表"""

    status_choice = (
        (1,'待执行'),
        (2,'正在执行'),
        (3,'已完成'),
        (4,'已撤单'),
        (5,'失败')
    )
    status = models.SmallIntegerField(verbose_name='订单状态',choices=status_choice,default=1)
    oid = models.CharField(verbose_name='订单号',max_length=64,unique=True)
    url = models.URLField(verbose_name='视频地址',db_index=True)
    count = models.IntegerField(verbose_name='数量')
    price = models.DecimalField(verbose_name='原价格',max_digits=10,decimal_places=2,default=0)
    real_price = models.DecimalField(verbose_name='实际价格',max_digits=10,decimal_places=2,default=0)
    old_view_count = models.CharField(verbose_name='原播放量',max_length=32,default='0')
    customer = models.ForeignKey(verbose_name='客户',to='Customer',on_delete=models.CASCADE)
    memo = models.CharField(verbose_name='备注',max_length=255,null=True,blank=True)

class TransactionRecord(BasicModel):

    charge_type_choice = (
        (1,'创建订单'),
        (2,'充值'),
        (3,'扣款'),
        (4,'撤单'),
        (5,'删除订单')
    )

    charge_type = models.SmallIntegerField(verbose_name='交易类型',choices=charge_type_choice)
    order_oid = models.CharField(verbose_name='订单号',max_length=64)
    amount = models.DecimalField('交易金额',max_digits=10,decimal_places=2,default=0)
    customer = models.ForeignKey(verbose_name='客户',to='Customer',on_delete=models.CASCADE)
    creator = models.ForeignKey(verbose_name='管理员',to='Administrator',on_delete=models.CASCADE,null=True,blank=True)
    memo = models.CharField(verbose_name='备注',max_length=255,null=True,blank=True)

2.2 迁移(在数据库生成相应的表)

django要使用orm操作mysql数据库,还需要先一些配置

  1. 安装pymysql

    pip install pymysql
    
  2. 在django项目的任意__init__文件内书写以下两行代码

    import pymysql
    
    pymysql.install_as_MySQLdb()
    
  3. 执行迁移到数据库命令

    # 先执行
    python manage.py makemigrations
    # 再执行
    python manage.py migrate
    
  4. 之后,我们数据库里就有对应的表了,可以用mysql数据库管理工具,比如navicat或者django自带的管理工具查看数据库了

    image-20230711150243801

    image-20230711150411268

    image-20230711150503658


3.登录

先做登录相关功能

包括:用户名密码登录和邮箱验证码登录

效果图:

image-20230711203645906

3.1 用户名密码登录

3.1.1 路由设计

由于我们项目较小,一个app足够就不做路由分发了

from django.urls import path
from web.view import account # 导入视图函数py文件

urlpatterns = [
    path('login/pwd/',account.login_pwd,name='login_pwd'),
]

3.1.2 视图函数逻辑

  1. app视图结构的介绍

    • 改动前

    image-20230711153817450

    • 改动后

    image-20230711154020824

  2. 登录视图函数逻辑代码

    from django.http import HttpResponse
    from  django.shortcuts import render,redirect
    from utils.forms.account import LoginPwdForm
    from web import models
    
    
    def login_pwd(request):
    
        # 1.当首次请求(get请求)这个url,返回用户名密码登录的前端页面
        if request.method == 'GET':
            # 1.1 由于登录页面涉及到用户填写信息之后再次上传数据,然后我们需要进行校验登录信息等操作,所以可以借助django中的forms组件
            # forms组件的三大功能:(1)生成页面可用的html页面 (2)对用户提交的数据进行校验 (3)保留用户上次输入内容并展示错误信息
            form_obj = LoginPwdForm()
            return render(request,'account/login_pwd.html',locals())
    
        # 2.当请求方式是post的时候,将用户提交的数据传入forms组件校验数据
        form_obj = LoginPwdForm(request.POST)
    
        # 3.判断传入数据是否合法
        if not form_obj.is_valid():
            return render(request,'account/login_pwd.html',locals())
    
        # 4.校验通过,可以从form_obj里取出数据,但是form_obj.cleaned_data就是一个字典形式,我们后面查数据库可以**打散形式传入当做条件比较方便,
        # 所以我们可以先pop出我们不需要的数据role,正好对role做一个判断去查哪一张表
        role = form_obj.cleaned_data.pop('role')
    
        # 5.由于role我们获取的值是1或者2,所以我们可以做个映射来判断当前登录用户是客户或者管理者,从而去对应的数据表里查是否存在该用户,
        # 也方便我们后期存储到session中,更加清晰明了
        role_mapping = {
            '1':'CUSTOMER',
            '2':'ADMIN'
        }
    
        # 插一点:当我们注册时,一定会把用户输入的密码转成密文然后在存入数据库,
        # 所以用户这里输入明文密码,我们需要将密码转成密文再去数据库校验,负责一直会校验不成功的!
        # 而我们可以在forms里做加密成密文,这样返回的form_obj.cleaned_data里面的密码就是密文的了,不需要在视图函数里操作了
    
        # 6.根据不同的角色,去查对应的表,filter直接链式操作,可以写多个filter,注意需要查询的是激活用户是否存在
        if role_mapping[role] == 'ADMIN':
            user_obj = models.Administrator.objects.filter(**form_obj.cleaned_data).filter(active=1).first()
        else:
            user_obj = models.Customer.objects.filter(**form_obj.cleaned_data).filter(active=1).first()
    
        # 7.如果不存在用户,报错用户名或者密码错误
        if not user_obj:
            form_obj.add_error('password','用户名或密码错误')
            return render(request,'account/login_pwd.html',locals())
    
        # 8.用户存在校验通过,将用户信息保存到session中
        request.session['user_info'] = {'role':role_mapping[role],'username':user_obj.username,'id':user_obj.id}
        # 9.保存成功跳转页面
        return HttpResponse('ok')
        # return redirect('/home/')
    

3.1.3 视图函数用到的forms组件

# 用户名密码登录forms组件
from django import forms
from utils.encrypt import md5

class LoginPwdForm(forms.Form):
    role = forms.ChoiceField(
        label='角色',
        choices=((1,'客户'),(2,'管理员')),
        widget=forms.Select(attrs={'class':'form-control'}),
        initial=1
    )

    username = forms.CharField(
        label='用户名',
        required=True,
        max_length=16,
        widget=forms.TextInput(attrs={'class':'form-control'}),
        error_messages={
            'required':'用户名不能为空',
            'max_length':'用户名最大16位',
        }
    )

    password = forms.CharField(
        label='密码',
        required=True,
        widget=forms.PasswordInput(attrs={'class':'form-control'}),
        error_messages={
            'required':'密码不能为空',
        }
    )


    # forms组件的钩子函数,它的执行顺序是password字段之后,所以一个可以在该函数内获取password数据
    def clean_password(self):
        password = md5(self.cleaned_data['password'])
        return password

3.1.4 前端页面搭建(借助了forms组件和bootstrap样式)

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <!-- 最新版本的 Bootstrap 核心 CSS 文件 -->
    <link rel="stylesheet" href="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/3.4.1/css/bootstrap.min.css"
          integrity="sha384-HSMxcRTRxnN+Bdg0JdbxYKrThecOKuH5zCYotlSAcp1+c8xmyTe9GYg1l9a69psu" crossorigin="anonymous">
    <style>
        body {
            background-color: #f0ffff;
        }

        .login-box {
            width: 20%;
            margin: 200px auto;
            border: 1px solid #ccc;
            border-radius: 10px;
            padding: 15px;
            box-shadow: 0 0 10px rgba(0, 0, 0, 0.5);
        }

        .pwd-sub a {
            float: right;
            line-height: 34px;
            height: 34px;
            text-decoration: none;
        }
        .pwd-sub{
            margin-top: 20px;
        }
    </style>
</head>
<body>
<!--登录盒子-->
<div class="login-box">
    <h3 style="text-align: center">登录</h3>
    <div class="pwd-form">
        <!--借助bootstrap的表单样式-->
        <form action="" method="post" novalidate>
            <!--清除django表单提交是产生的csrf影响-->
            {% csrf_token %}
            <!--利用forms组件循环生成input框-->
            {% for form in form_obj %}
                <div class="form-group">
                    <label for="{{ form.id_for_label }}">{{ form.label }}:</label>
                    {{ form }}
                <span style="color: red;float:right;">{{ form.errors.0 }}</span>
                </div>
            {% endfor %}
            <div class="pwd-sub">
                <button type="submit" class="btn btn-primary">登录</button>
                <a href="">邮箱登录</a>
            </div>
        </form>
    </div>
</div>
</body>
</html>

3.1.5 密码登录涉及的补充知识点

  1. django中forms组件的使用

  2. md5加密加盐

  3. session的使用(作用是为了后面的登录和权限校验进行校验的)

  4. 前端知识补充

    • 如何让一个盒子的边框变成圆角

      border-radius: 10px;
      
    • 如何让一个盒子在页面显示更加立体(有阴影感)

      box-shadow: 0 0 10px rgba(0, 0, 0, 0.5);
      

3.2 邮箱验证码登录

3.2.1 路由设计

    path('login/email/', account.login_email, name='login_email'),

3.2.2 视图函数逻辑

邮箱验证码登录涉及到两个视图函数,分别是当的点击获取验证码时的一个和点击登录的一个

  • 获取验证码视图函数

    def mail_email(request):
    
        # 自定义的响应类
        res = BasicResponse()
        # 可以再定义一个forms组件,用来处理提交过来的角色和邮箱的校验
        form_obj = MailEmailForm(request.POST)
    
        if not form_obj.is_valid():
            # 由于这里是ajax的请求,我们在返回给前端时,不能像上面哪有在返回一个html页面,需要返回一个json字符串
            # 我们可以自定义一个响应类,然后调用该响应类的一个方法,来规定返回的数据格式
            res.detail = form_obj.errors # form_obj.errors是一个字典,里面是错误信息
            return JsonResponse(res.dict) # JsonResponse里面必须传一个字典,传列表需要再指定一个safe=False参数
    
        # 先生成随机验证码
        code = str(random.randint(1000,9999))
        code_msg = '您的验证码为{},一分钟内有效!'.format(code)
    
        # 调用发送邮箱验证码接口(当然这些也可以放在forms组件里处理)
        email = form_obj.cleaned_data['email']
        status = send_email('15822539779@163.com',email,code_msg)
        if not status:
            form_obj.add_error('email','发送失败,请重试')
            res.detail = form_obj.errors
            return JsonResponse(res.dict)
    
        # 发送成功,保存验证码信息到redis里,以便后面登录校验验证码
        conn = get_redis_connection('default')
        conn.set(email,code,ex=60)
        res.status = True
        return JsonResponse(res.dict)
    
    • 该视图函数里用到的自定义响应类

      class BasicResponse(object):
      
          def __init__(self):
              self.status = False
              self.detail = None
              self.data = None
      
          @property
          def dict(self):
              return self.__dict__
      
    • 用到的发送邮箱验证码函数

      import smtplib
      from email.mime.text import MIMEText
      from email.utils import formataddr
      
      def send_email(send_user,recv_user,code_msg):
      
          try:
              # ### 1.邮件内容配置 ###
              msg = MIMEText(code_msg, 'html', 'utf-8')  # 邮件内容验证码
              msg['From'] = formataddr(["订单交易系统", "158****9779@163.com"]) # 邮件头部(谁发送的)
              msg['Subject'] = "邮箱登录验证码" # 邮件主题
      
              # ### 2.发送邮件 ###
              server = smtplib.SMTP_SSL("smtp.163.com") # 网易的那个smtp服务器
              server.login(send_user, "RCJEEXBFPKXTXRGZ") # 两个参数分别是自己的邮箱和邮箱的授权码
               # 三个参数分别是发送人邮箱、收件人邮箱,邮件内容!
              server.sendmail(send_user, recv_user, msg.as_string())
              server.quit()
              return True
          except Exception as e:
              return False
      
    • 用到的forms组件

      class MailEmailForm(forms.Form):
          role = forms.ChoiceField(
              label='角色',
              initial=1,
              choices=((1,'客户'),(2,'管理员')),
              widget=forms.Select(attrs={'class':'form-control'})
          )
      
      
          email = forms.EmailField(
              label='邮箱',
              required=True,
              widget=forms.EmailInput(attrs={'class': 'form-control', 'placeholder': '请输入邮箱'}),
              error_messages={
                  'required': '邮箱不能为空',
                  'invalid': '邮箱格式不正确'
              }
          )
      
          # 根据角色和邮箱信息去判断数据库是否有该用户信息(该用户有没有注册过)
          def clean(self):
              # 注意这里获取值用get方法,判断其是否有值,因为如果email格式不正确,self.cleaned_data是没有email的值的
              role = self.cleaned_data.get('role')
              email = self.cleaned_data.get('email')
      
              # 判断如果两个其中有一个没值就返回
              if not (role and email):
                  return self.cleaned_data
      
              if role == '1':
                  is_exist = models.Customer.objects.filter(active=1,email=email).exists()
              else:
                  is_exist = models.Administrator.objects.filter(active=1, email=email).exists()
      
              if not is_exist:
                  self.add_error('email','该邮箱还未注册,请先注册')
              return self.cleaned_data
      
  • 点击登录按钮视图函数

    def login_email(request):
    
        if request.method == 'GET':
            form_obj = LoginEmailForm()
            return render(request,'account/login_email.html',locals())
    
        res = BasicResponse()
        # post请求处理逻辑
        form_obj = LoginEmailForm(request.POST)
        if not form_obj.is_valid():
            res.detail = form_obj.errors
            return JsonResponse(res.dict)
    
        role = form_obj.cleaned_data.pop('role')
        email = form_obj.cleaned_data['email']
    
        mapping = {"1": "ADMIN", "2": "CUSTOMER"}
        if mapping[role] == 'CUSTOMER':
            user_obj = models.Customer.objects.filter(active=1,email=email).first()
        else:
            user_obj = models.Administrator.objects.filter(active=1,email=email).first()
    
        if not user_obj:
            form_obj.add_error('email','邮箱不存在')
            res.detail = form_obj.errors
            return JsonResponse(res.dict)
    
        request.session['user_info'] = {'role':mapping[role],'username':user_obj.username,'id':user_obj.id}
        res.status = True
        # 返回登录成功需要跳转的url,前端借用js语法实现跳转
        res.data = '/home/'
        return JsonResponse(res.dict)
    
    • 用到的forms组件

      class LoginEmailForm(forms.Form):
          role = forms.ChoiceField(
              label='角色',
              initial=1,
              choices=((1,'客户'),(2,'管理员')),
              widget=forms.Select(attrs={'class':'form-control'})
          )
      
          email = forms.EmailField(
              label='邮箱',
              required=True,
              widget=forms.EmailInput(attrs={'class':'form-control','placeholder':'请输入邮箱'}),
              error_messages={
                  'required':'邮箱不能为空',
                  'invalid':'邮箱格式不正确'
              }
          )
      
          code = forms.CharField(
              label='验证码',
              required=True,
              max_length=4,
              min_length=4,
              widget=forms.TextInput(attrs={'class':'form-control','placeholder':'请输入验证码'}),
              error_messages={
                  'required':'验证码不能为空',
                  'max_length':'验证码最大4位',
                  'min_length': '验证码最小4位',
              }
          )
      
          def clean_code(self):
              email = self.cleaned_data.get('email','')
              code = self.cleaned_data['code']
              if not email:
                  return code
      
              conn = get_redis_connection('default')
              redis_code = conn.get(email).decode('utf-8')
              if not redis_code:
                  raise ValidationError( '验证码未发送或已失效')
      
              if code != redis_code:
                  self.add_error('code', '验证码不正确')
      

3.1.3 前端页面搭建(涉及ajax提交数据)

{% load static %}
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <!-- 最新版本的 Bootstrap 核心 CSS 文件 -->
    <link rel="stylesheet" href="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/3.4.1/css/bootstrap.min.css"
          integrity="sha384-HSMxcRTRxnN+Bdg0JdbxYKrThecOKuH5zCYotlSAcp1+c8xmyTe9GYg1l9a69psu" crossorigin="anonymous">
    <style>
        body {
            background-color: #f0ffff;
        }

        .login-box {
            width: 20%;
            margin: 200px auto;
            border: 1px solid #ccc;
            border-radius: 10px;
            padding: 15px;
            box-shadow: 0 0 10px rgba(0, 0, 0, 0.5);
        }

        .login-sub a {
            float: right;
            line-height: 34px;
            height: 34px;
            text-decoration: none;
        }

        .login-sub {
            margin-top: 20px;
        }

        .col-md-4 {
            margin-left: -15px;
            margin-right: -15px;
        }

    </style>
</head>
<body>
<!--登录盒子-->
<div class="login-box">
    <h3 style="text-align: center">登录</h3>
    <div class="login-form">
        <!--借助bootstrap的表单样式-->
        <form action="" method="post" novalidate id="email-form">
            <!--清除django表单提交是产生的csrf影响-->
            {% csrf_token %}
            <!--利用forms组件循环生成input框-->
            {% for form in form_obj %}
                <!--这里不同于密码登录的是:验证码登录布局是一行左边是输入框,右边是获取验证码按钮-->
                {% if form.name == 'code' %}
                    <div class="form-group">
                        <label for="{{ form.id_for_label }}">{{ form.label }}:</label>
                        <div class="row">
                            <div class="col-md-8">
                                {{ form }}
                            </div>
                            <div class="col-md-4">
                                <input type="button" value="获取验证码" class="form-control sms-btn">
                            </div>
                        </div>
                    </div>
                {% else %}
                    <div class="form-group">
                        <label for="{{ form.id_for_label }}">{{ form.label }}:</label>
                        {{ form }}
                        <span style="color: red;float:right;" class="error-msg">{{ form.errors.0 }}</span>
                    </div>
                {% endif %}
            {% endfor %}
            <div class="login-sub">
                <button type="button" class="btn btn-primary email-sub">登录</button>
                <span style="color: red;" id="code-msg" class="error-msg">{{ form.errors.0 }}</span>
                <a href="{% url 'login_pwd' %}">密码登录</a>
            </div>
        </form>
    </div>
</div>

<!--引入jquery-->
<script src="{% static 'plugins/jquery.js' %}"></script>
<!--ajax提交数据给后端时,去除没有csrftoken的影响-->
<script src="{% static 'js/csrf.js' %}"></script>
<script>
    $(function () {

        getCodeEvent();

        emailSubEvent();

        clickInput();

    })

    // 给获取验证码按钮添加一个点击事件
    function getCodeEvent() {
        $('.sms-btn').click(function () {
            // 获取邮箱和角色信息给后端去校验,然后调用发送邮箱验证码
            $.ajax({
                url: '{% url "mail_email" %}',
                type: 'post',
                data: {'role': $('#id_role').val(), 'email': $('#id_email').val()},
                dataType: 'json',
                success: function (res) {
                    if (res.status) {
                        // 发送验证码成功,修改获取验证码按钮状态为禁用,并让它60s后才能使用
                        alterSmsBtn();
                    } else {
                        // status状态为false,我们循环detail,来获取错误信息,展示到页面上
                        $.each(res.detail, function (k, v) {
                            $('#id_' + k).next().text(v[0])
                        })
                    }
                }
            })
        })
    }

    // 修改验证码框事件
    function alterSmsBtn() {
        $('.sms-btn').prop('disabled', true);
        var time = 60
        var ID = setInterval(function () {
            if (time >= 1) {
                $('.sms-btn').val(time + '秒重试');
                time--;
            } else {
                clearInterval(ID);
                $('.sms-btn').prop('disabled', false).val('获取验证码')
            }
        }, 1000)
    }

    // 点击登录按钮事件
    function emailSubEvent() {
        $('.email-sub').click(function () {
            $.ajax({
                url: '{% url "login_email" %}',
                type: 'post',
                data: $('#email-form').serialize(), // 这种方式是序列化form表单内容(不包括文件)
                dataType: 'json',
                success: function (res) {
                    if (res.status) {
                        location.href = res.data
                    } else {
                        $.each(res.detail, function (k, v) {
                            if (k == 'code') {
                                $('#code-msg').text(v[0])
                            } else {
                                $('#id_' + k).next().text(v[0])
                            }

                        })
                    }
                }
            })
        })
    }

    // 给每个input绑定一个点击事件,一点击消除错误信息
    function clickInput() {
        $('input').click(function () {
            $('.error-msg').text('')
        })
    }

</script>
</body>
</html>

对与两种登录共用的样式我们可以放到一个公共的css文件中


4.登录校验和权限校验

对于网站的一些页面,我们需要用户进行是否登录判断,没有登录的用户是不能访问的;

而对于登录用户,我们又需要一些判断,判断其有没有权限访问该页面

4.1 登录校验

在做完上述的登录功能之后,我们最后将登录成功的用户信息保存到了session中,并且设置了过期时间(一般是2周,可以自行修改),下次该用户请求网站的其他页面时,都会修改该session信息

如果没有携带session信息,我们就可以访问他没有登录,对于需要登录才能访问的页面,就跳转到登录页面提醒他登录

对于登录和权限校验,这些功能基本上每个url都需要进行,所以,我们可以把这个功能放到中间件里面去,中间件就是django的门户,每次请求都要先经过中间件

  1. 在utils里新建一个py文件,用来编写自定义的中间件

    image-20230712092212426

  2. 在新建的py文件里编写自定义中间件类,该类继承MiddlewareMixin,并且根据需要编写中间件的5个方法

    from django.utils.deprecation import MiddlewareMixin
    from django.shortcuts import render,redirect,reverse
    from django.conf import settings
    
    class UserInfo(object):
        def __init__(self,id,username,role):
            self.id = id
            self.username = username
            self.role = role
    
    
    class LoginPermissionMd(MiddlewareMixin):
    
        # 请求来的时候得经过该方法
        def process_request(self,request):
    
            # 登录校验:根据session里是否有我们存储的key,来判断用户是否登录
    
            # 1.登录白名单,在白名单里面的url不需要登录就可以访问
            # 1.1 拿到当前url
            current_url = request.path_info
            if current_url in settings.LOGIN_WHITE_LIST:
                return
    
            # 2.不在登录白名单,去session取用户信息,取不到跳转到登录页面
            user_info_dict = request.session.get('user_info')
            if not user_info_dict:
                return redirect(reverse('login_pwd'))
    
            # 3.取到了用户登录信息,将其保存到request里,方便后续使用
            # 我们定义了一个用户类,用来存用户登录信息,然后将其赋值给request,这样方便我们后续用.的方式取用户信息
            user_obj = UserInfo(**user_info_dict)
            request.user = user_obj
    
  3. settings里面配置的登录白名单

    # 登录白名单
    LOGIN_WHITE_LIST = ['/login/pwd/']
    

4.2 权限校验

对于登录用户,登录信息已保存到request中,我们可以根据登录的角色来给他们赋予不同的权限(xxx有啥权限对应的url和name)(在settings里配置)

4.2.1 初版思路

  • settings
# 用户权限配置
PERMISSION_MENU = {
    'ADMIN':[
        'home',
    ],
    'CUSTOMER':[
        'home',
    ]
}
  • 权限校验的中间件
    def process_view(self, request, callback, callback_args, callback_kwargs):
        """
        对于权限校验,我们其实可以在process_request就进行校验的
        根据request.path_info和已登录用户的role信息去配置文件里查相应的role有没有访问该url的权限
        但是,我们在process_view里面进行权限校验的好处是我们可以根据url对应的name来判断role是否有访问name的权限,而不是根据url判断了
        (因为,进入了process_view的url都是经过了路由层匹配了,我们可以根据request.resolver_match来获取url对象的name等信息)
        (当url里面有不定参数时,我们就更不好根据url进行权限校验了)
        """

        # 对于登录白名单就不需要在进行权限校验了直接return即可
        current_url = request.path_info
        if current_url in settings.LOGIN_WHITE_LIST:
            return

        # 拿到当前访问url的name
        current_name = request.resolver_match.url_name
        # 拿到当前登录用户的角色
        role = request.user.role
        # 根据角色去配置查询角色所拥有的权限
        user_permission_list = settings.PERMISSION_MENU[role]
        # 无权访问调转无权访问页面
        if current_name not in user_permission_list:
            return render(request,'permission.html')

4.2.2 加入当子菜单被点击时,主菜单添加被选中功能的设计

  • 修改settings权限配置


5.主页模板设计

设计完登录功能之后,我们登录成功就需要调转到个人主页页面,对于个人主页,我们的设计是顶部有导航栏,右边有侧边菜单栏

因为我们后续的订单管理,用户管理等页面都需要侧边菜单栏和导航栏,所以对于导航栏和菜单栏我们可以设置成母版。

5.1 顶部导航栏

顶部的导航栏就是借用的bootstrap的导航条

  <div class="row">
        <nav class="navbar navbar-default navbar-fixed-top ">
            <div class="container-fluid">
                <!-- Brand and toggle get grouped for better mobile display -->
                <div class="navbar-header">
                    <button type="button" class="navbar-toggle collapsed" data-toggle="collapse"
                            data-target="#bs-example-navbar-collapse-1" aria-expanded="false">
                        <span class="sr-only">Toggle navigation</span>
                        <span class="icon-bar"></span>
                        <span class="icon-bar"></span>
                        <span class="icon-bar"></span>
                    </button>
                    <a class="navbar-brand" href="#">订单交易系统</a>
                </div>

                <!-- Collect the nav links, forms, and other content for toggling -->
                <div class="collapse navbar-collapse" id="bs-example-navbar-collapse-1">
                    <ul class="nav navbar-nav">
                        <li class="active"><a href="#">Link <span class="sr-only">(current)</span></a></li>
                        <li><a href="#">Link</a></li>
                        <li class="dropdown">
                            <a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button"
                               aria-haspopup="true"
                               aria-expanded="false">Dropdown <span class="caret"></span></a>
                            <ul class="dropdown-menu">
                                <li><a href="#">Action</a></li>
                                <li><a href="#">Another action</a></li>
                                <li><a href="#">Something else here</a></li>
                                <li role="separator" class="divider"></li>
                                <li><a href="#">Separated link</a></li>
                                <li role="separator" class="divider"></li>
                                <li><a href="#">One more separated link</a></li>
                            </ul>
                        </li>
                    </ul>
                    <form class="navbar-form navbar-left">
                        <div class="form-group">
                            <input type="text" class="form-control" placeholder="Search">
                        </div>
                        <button type="submit" class="btn btn-default">搜索</button>
                    </form>
                    <ul class="nav navbar-nav navbar-right">
                        <li><img src="{% static 'img/default.jpg' %}" alt="" width="40px" height="40px"
                                 style="border-radius: 50%;margin-top: 5px"></li>
                        <li class="dropdown">
                            <a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button"
                               aria-haspopup="true"
                               aria-expanded="false">账号设置 <span class="caret"></span></a>
                            <ul class="dropdown-menu">
                                <li><a href="#">账户信息</a></li>
                                <li><a href="#">退出登录</a></li>
                                <li><a href="#">重置密码</a></li>
                            </ul>
                        </li>
                    </ul>
                </div><!-- /.navbar-collapse -->
            </div><!-- /.container-fluid -->
        </nav>
    </div>

5.2 侧边栏菜单

首先,我们需要设定不同的登录用户类型拥有的菜单是不同的,因此,侧边栏的div应该是动态的,需要一定的逻辑处理生成html,使用可以使用inclusion_tag设计

# 母版html侧边栏菜单写法
<div class="col-md-2">
	{% left_menu request %}
</div>

inclusion_tag

from django import template
from django.conf import settings

register = template.Library()


@register.inclusion_tag('tags/left_menu.html')
def left_menu(request):
    # 登录home页面携带这request参数,将request参数传入inclusion_tag
    # 我们需要根据request里面role信息去查询settings里面该用户有啥菜单,并且菜单的url和name1是啥
    role = request.user.role
    user_permission_list = settings.LEFT_MENU_PERMISSION[role]
    return locals()

left_menu.html

<div class="list-group">
    {% for item in user_permission_list %}
        <a href="#" class="list-group-item list-group-item-info">
            <span class="fa {{ item.icon }}"> {{ item.text }}</span>
        </a>
        {% for child in item.children %}
            <a href="{{ child.url }}" class="list-group-item">{{ child.text }}</a>
        {% endfor %}
    {% endfor %}
</div>

settings配置菜单

# 用户侧边菜单栏权限
LEFT_MENU_PERMISSION = {
    'ADMIN':[
        {
            'text':'用户管理',
            'icon':'fa-address-card-o',
            'children':[
                {'text':'用户信息','url':'','name':''},
                {'text':'订单管理','url':'','name':''},
            ]
        },
        {
            'text': '交易管理',
            'icon': 'fal fa-cart-plus',
            'children': [
                {'text': '用户信息', 'url': '', 'name': ''},
                {'text': '订单管理', 'url': '', 'name': ''},
            ]
        }
    ]
}

5.3 新思路:导航栏和侧边菜单栏合并

# 导航栏兼具侧边菜单栏
<div class="nav">
    {% top_nav request %}
</div>

# 导航栏inclusion_tag
{% load static %}
<nav class="navbar navbar-default navbar-fixed-top ">
    <div class="container-fluid">
        <!-- Brand and toggle get grouped for better mobile display -->
        <div class="navbar-header">
            <button type="button" class="navbar-toggle collapsed" data-toggle="collapse"
                    data-target="#bs-example-navbar-collapse-1" aria-expanded="false">
                <span class="sr-only">Toggle navigation</span>
                <span class="icon-bar"></span>
                <span class="icon-bar"></span>
                <span class="icon-bar"></span>
            </button>
            <a class="navbar-brand" href="#">订单交易系统</a>
        </div>

        <!-- Collect the nav links, forms, and other content for toggling -->
        <div class="collapse navbar-collapse" id="bs-example-navbar-collapse-1">
            <ul class="nav navbar-nav">
                {% for item in user_permission_list %}
                    <li class="dropdown fa">
                        <a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button"
                           aria-haspopup="true"
                           aria-expanded="false"><span class="fa {{ item.icon }}"> {{ item.text }}</span> <span class="caret"></span></a>
                        <ul class="dropdown-menu">
                            {% for child in item.children %}
                                <li role="separator" class="divider"></li>
                                <li><a href="{{ child.url }}">{{ child.text }}</a></li>
                            {% endfor %}
                        </ul>
                    </li>
                {% endfor %}
            </ul>
            <form class="navbar-form navbar-left">
                <div class="form-group">
                    <input type="text" class="form-control" placeholder="Search">
                </div>
                <button type="submit" class="btn btn-default">搜索</button>
            </form>
            <ul class="nav navbar-nav navbar-right">
                <li><img src="{% static 'img/default.jpg' %}" alt="" width="40px" height="40px"
                         style="border-radius: 50%;margin-top: 5px"></li>
                <li class="dropdown">
                    <a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button"
                       aria-haspopup="true"
                       aria-expanded="false">账号设置 <span class="caret"></span></a>
                    <ul class="dropdown-menu">
                        <li><a href="#">账户信息</a></li>
                        <li><a href="#">退出登录</a></li>
                        <li><a href="#">重置密码</a></li>
                    </ul>
                </li>
            </ul>
        </div><!-- /.navbar-collapse -->
    </div><!-- /.container-fluid -->
</nav>

6.级别表的增删改查页面

6.1 级别的展示

1.视图层

查询到未被删除的级别传入模板层

def level_list(request):
    level_queryset = models.Level.objects.filter(active=1)
    return render(request,'level/level_list.html',locals())

2.模板层

级别列表的html只需要继承母版就行,在可修改的内部部分放一个table,循环展示级别信息

{% extends 'layout.html' %}


{% block content %}
    <div class="table-responsive">
        <a href="{% url 'level_add' %}">
            <button class="btn btn-success" style="margin-bottom: 5px">
                <span class="glyphicon glyphicon-plus-sign">新建级别</span>
            </button>
        </a>
        <table class="table table-bordered table-striped">
            <thead>
            <tr>
                <th>ID</th>
                <th>级别</th>
                <th>折扣</th>
                <th>操作</th>
            </tr>
            </thead>
            <tbody>
            {% for obj in level_queryset %}
                <tr>
                    <th scope="row">{{ forloop.counter }}</th>
                    <td>{{ obj.title }}</td>
                    <td>{{ obj.percent }}%</td>
                    <td>
                        <a href="{% url 'level_edit' pk=obj.id %}">
                            <button class="btn btn-xs btn-info">编辑</button>
                        </a>
                        <button class="btn btn-xs btn-danger del-btn" row-id="{{ obj.id }}">删除</button>
                    </td>
                </tr>
            {% endfor %}
            </tbody>
        </table>
    </div>

    <!-- Modal -->
    <div class="modal fade" id="delModal" tabindex="-1" role="dialog" aria-labelledby="myModalLabel">
        <div class="modal-dialog" role="document">
            <div class="modal-content">
                <div class="modal-header">
                    <button type="button" class="close" data-dismiss="modal" aria-label="Close"><span
                            aria-hidden="true">&times;</span></button>
                </div>
                <div class="modal-body" style="text-align: center">
                    确定删除当前内容吗?删除后无法复原!
                </div>
                <div class="modal-footer">
                    <span class="del_error_msg" style="color: red"></span>
                    <button type="button" class="btn btn-default" data-dismiss="modal">取消</button>
                    <button type="button" class="btn btn-primary confirm-del">确定</button>
                </div>
            </div>
        </div>
    </div>
{% endblock %}

{% block js %}
    <script>
        $(function () {
            // 1.点击删除按钮,弹出确认删除弹出框
            $('.del-btn').click(function () {
                $('.del_error_msg').empty()
                var Rid = $(this).attr('row-id')
                $('#delModal').modal('show');
                // 点击确定向后端发送ajax请求,删除
                $('.confirm-del').click(function () {
                    $.ajax({
                        url: '{% url "level_del" %}',
                        type: 'get',
                        data: {rid: Rid},
                        dataType: 'json',
                        success: function (res) {
                            if (res.status) {
                                location.href = res.data;
                            } else {
                                $('.del_error_msg').text(res.detail)
                            }
                        }
                    })
                })
            })
        })
    </script>
{% endblock %}

上述还包括一个删除模态框和对应的js代码,后期我们可以考虑将他封装起来以便后续使用

6.2 新建级别

1.视图函数

新建级别涉及到用户输入,然后需要将提交的信息进行校验,操作数据库保存,所以可以考虑用到form组件,这里我们换一种使用modelform

def level_add(request):

    if request.method == 'GET':
        form_obj = LevelAddForm()
        return render(request, 'form.html', locals())

    form_obj = LevelAddForm(request.POST)
    if not form_obj.is_valid():
        return render(request, 'form.html', locals())

    form_obj.save()

    return redirect(reverse('level_list'))

2.对应的modelform组件

class LevelAddForm(forms.ModelForm):

    class Meta:
        model = models.Level
        fields = ['title','percent']
        widgets = {
            'title':wid.TextInput(attrs={'class':'form-control','placeholder':'请输入级别'}),
            'percent': wid.TextInput(attrs={'class': 'form-control', 'placeholder': '请输入折扣'}),
        }
        help_texts = {
            'percent':'请填入0-100的整数表示百分比,例如90,表示90%'
        }
        error_messages = {
            'title':{'required':'级别不能为空'},
            'percent':{
                'required':'折扣不能为空',

            }
        }

    def clean_percent(self):
        percent = self.cleaned_data['percent']
        if percent >= 100:
            raise ValidationError('输入的整数应该小于100')
        return percent

3.模板层

模板层的html也可以继承母版,在可修改的内容部分,添加form表单,根据modelform生成input输入框

{% extends 'layout.html' %}


{% block content %}
    <form method="post" action="" novalidate>
    {% csrf_token %}
        {% for form in form_obj %}
            <div class="form-group" style="position: relative;margin-bottom: 25px">
                <label for="{{form.id_for_label }}">{{ form.label }}</label>
                {% if form.help_text %}
                    <span>({{ form.help_text }})</span>
                {% endif %}
                {{ form }}
                <span style="color: red;position: absolute">{{ form.errors.0 }}</span>
            </div>
        {% endfor %}
        <input type="submit" value="保存" class="btn btn-success">
    </form>
{% endblock %}

{% block js %}
    <script>
        $('input').click(function (){
            $(this).next().empty()
        })
    </script>
{% endblock %}

6.3 编辑级别

1.视图函数

前端需要把要编辑的id传给后端,后端根据id查找是否存在该数据,然后将该数据返回到带修改的input框内

def level_edit(request,pk):
    res = BasicResponse()
    level_obj = models.Level.objects.filter(active=1,id=pk).first()
    if not level_obj:
        res.detail = {'error':'要编辑的内容不存在'}
        return JsonResponse(res.dict,json_dumps_params={'ensure_ascii':False})

    if request.method == 'GET':
        form_obj = LevelAddForm(instance=level_obj)
        return render(request,'form.html',locals())

    form_obj = LevelAddForm(data=request.POST,instance=level_obj)
    if not form_obj.is_valid():
        return render(request, 'form.html', locals())
    form_obj.save()
    return redirect(reverse('level_list'))

2.模板层

共用新建级别的form.html,后续关于需要用户输入和进行校验用户输入的都可以共用该html,该html会根据传入的modelform不同,渲染出input框

6.4 删除级别

1.视图层

删除我们用的ajax提交,根据提交的删除id,查找数据库该数据是否存在,存在则进行逻辑删除,然后返回jsonresponse

def level_del(request):
    res = BasicResponse()
    rid = request.GET.get('rid')
    level_obj = models.Level.objects.filter(active=1,id=rid).first()
    if not level_obj:
        res.detail = '要删除的数据不存在'
        print(111)
        return JsonResponse(res.dict)
    level_obj.active=0
    level_obj.save()
    res.status =True
    res.data = '/level/list/'
    return JsonResponse(res.dict)

2.模板层

删除的ajax和弹出删除确认框见列表级别的html

后续这个可以封装,以后哪里需要在引入即可!

7.客户管理

基本同级别管理略

效果图:

image-20230719085623854

后期会加搜索框和筛选功能

8.价格管理

基本同级别管理略

效果图:

image-20230719085802818

9.交易记录

基本同级别管理略

效果图:

image-20230719090009846

9.1 充值扣款功能

当点击某个客户的交易记录,会跳转该客户的交易记录列表页面,在该页面可以,点击扣款或者充值按钮,给该客户充值或者扣款

效果图:

image-20230719090431185

充值和扣款需要涉及两张表,所以我们需要用到事务和锁的概念

视图逻辑代码:

def customer_transaction(request,pk):

    res = BasicResponse()

    if request.method == 'GET':
        transaction_queryset = models.TransactionRecord.objects.filter(active=1,customer_id=pk)
        form_obj = TransactionAddForm()
        return render(request, 'transactions/transaction_list.html', locals())

    form_obj = TransactionAddForm(data=request.POST)
    if not form_obj.is_valid():
        res.detail = form_obj.errors
        return JsonResponse(res.dict)

    try:
        with transaction.atomic():
            charge_type = form_obj.cleaned_data['charge_type']
            amount = form_obj.cleaned_data['amount']
            customer_obj = models.Customer.objects.filter(active=1,id=pk).first()
            if charge_type == '3' and customer_obj.balance < amount:
                res.detail = {'amount':['操作失败,当前账号只有{}余额'.format(customer_obj.balance)]}
                return JsonResponse(res.dict)
            # 生成订单号
            while True:
                random_str = str(random.randint(1000,9999))
                oid = datetime.now().strftime('%Y%m%d%H%M%S%f') + random_str
                is_exist = models.TransactionRecord.objects.filter(active=1,order_oid=oid).exists()
                if is_exist:
                    continue
                break
            # 操作交易记录表生成交易记录
            models.TransactionRecord.objects.create(
                charge_type=charge_type,
                order_oid=oid,
                amount=amount,
                customer_id=pk,
                creator_id=request.user.id
            )

            # 操作客户表
            if charge_type == '2':
                models.Customer.objects.filter(active=1,id=pk).update(balance=F('balance')+amount)
            else:
                models.Customer.objects.filter(active=1, id=pk).update(balance=F('balance') - amount)
    except Exception as e:
        res.detail={'amount':['操作失败,请重试']}
        return JsonResponse(res.dict)

    res.status =True
    return JsonResponse(res.dict)

前端代码:

{% extends 'layout.html' %}

{% block content %}
    <div class="table-responsive">
        {% if pk %}
            <button class="btn btn-success transaction-add-btn" style="margin-bottom: 5px">
                <span class="glyphicon glyphicon-plus-sign">充值/扣款</span>
            </button>
        {% endif %}
        <table class="table table-bordered table-striped">
            <thead>
            <tr>
                <th>ID</th>
                <th>交易类型</th>
                <th>订单号</th>
                <th>交易金额</th>
                <th>创建时间</th>
                <th>客户</th>
                <th>管理员</th>
                <th>其他</th>
            </tr>
            </thead>
            <tbody>
            {% for obj in transaction_queryset %}
                <tr>
                    <th scope="row">{{ forloop.counter }}</th>
                    <td>{{ obj.get_charge_type_display }}</td>
                    <td>{{ obj.order_oid }}</td>
                    <td>{{ obj.amount }}</td>
                    <td>{{ obj.create_date|date:'Y:m:d H:i:s' }}</td>
                    <td>{{ obj.customer.username }}</td>
                    <td>{{ obj.creator.username }}</td>
                    <td>{{ obj.meno }}</td>
                </tr>
            {% endfor %}
            </tbody>
        </table>
    </div>


    <!-- Modal -->
    <div class="modal fade" id="addTransactionModal" tabindex="-1" role="dialog" aria-labelledby="myModalLabel">
        <div class="modal-dialog" role="document">
            <div class="modal-content">
                <div class="modal-header">
                    <button type="button" class="close" data-dismiss="modal" aria-label="Close"><span
                            aria-hidden="true">&times;</span></button>
                    <h4 class="modal-title" id="myModalLabel">添加交易记录</h4>
                </div>
                <div class="modal-body">
                    <form class="form-horizontal" id="transaction-form">
                        {% for form in form_obj %}
                            {% csrf_token %}
                            <div class="form-group">
                                <label for="{{ form.id_for_label }}" class="col-sm-2 control-label">{{ form.label }}</label>
                                <div class="col-sm-10">
                                    {{ form }}
                                    <span style="color: red;float:right;" class="error-msg">{{ form.errors.0 }}</span>
                                </div>
                            </div>
                        {% endfor %}
                    </form>
                </div>
                <div class="modal-footer">
                    <button type="button" class="btn btn-default" data-dismiss="modal">取消</button>
                    <button type="button" class="btn btn-primary confirm-transaction">确定</button>
                </div>
            </div>
        </div>
    </div>

{% endblock %}

{% block js %}
    <script>
        $(function () {
            $('.transaction-add-btn').click(function () {
                $('.error-msg').empty()
                $('#addTransactionModal').modal('show');
                $('.confirm-transaction').click(function (){
                    $.ajax({
                        url:'',
                        type:'post',
                        data:$('#transaction-form').serialize(),
                        dataType:'json',
                        success:function (res){
                            if(res.status){
                                $('#addTransactionModal').modal('hide');
                            }else {
                                $.each(res.detail,function (k,v){
                                    $('#id_'+k).next().text(v[0])
                                })
                            }
                        }
                    })
                })
            })
        })

    </script>
{% endblock %}

10.订单管理(客户功能)

10.1 订单列表的展示

效果图:

image-20230719135031983

1.视图逻辑代码

返回当前登录用户相关的订单

def order_list(request):

    order_queryset = models.Order.objects.filter(active=1,customer_id=request.user.id)

    return render(request,'order/order_list.html',locals())

2.前端页面代码

细节处理:当用户订单的状态不是待执行时,不应该显示撤单按钮

{% extends 'layout.html' %}


{% block content %}
    <div class="table-responsive">
        <a href="{% url 'order_add' %}">
            <button class="btn btn-success" style="margin-bottom: 5px">
                <span class="glyphicon glyphicon-plus-sign">下单</span>
            </button>
        </a>
        <table class="table table-bordered table-striped">
            <thead>
            <tr>
                <th>ID</th>
                <th>订单号</th>
                <th>视频地址</th>
                <th>数量</th>
                <th>原价格(实际价格)</th>
                <th>原播放量</th>
                <th>订单状态</th>
                <th>操作</th>
            </tr>
            </thead>
            <tbody>
            {% for obj in order_queryset %}
                <tr>
                    <th scope="row">{{ forloop.counter }}</th>
                    <td>{{ obj.oid }}</td>
                    <td>{{ obj.url }}</td>
                    <td>{{ obj.count }}</td>
                    <td>{{ obj.price }}({{ obj.real_price }})</td>
                    <td>{{ obj.old_view_count }}</td>
                    <td>{{ obj.get_status_display }}</td>
                    {% if obj.status == 1 %}
                        <td>
                            <button class="btn btn-danger btn-xs order-rm-btn" row-id="{{ obj.id }}">撤单</button>
                        </td>
                    {% else %}
                        <td>-</td>
                    {% endif %}
                </tr>
            {% endfor %}
            </tbody>
        </table>
    </div>

    <!-- Modal -->
    <div class="modal fade" id="delModal" tabindex="-1" role="dialog" aria-labelledby="myModalLabel">
        <div class="modal-dialog" role="document">
            <div class="modal-content">
                <div class="modal-header">
                    <button type="button" class="close" data-dismiss="modal" aria-label="Close"><span
                            aria-hidden="true">&times;</span></button>
                </div>
                <div class="modal-body" style="text-align: center">
                    确定撤销当前订单吗?
                </div>
                <div class="modal-footer">
                    <span class="del_error_msg" style="color: red"></span>
                    <button type="button" class="btn btn-default" data-dismiss="modal">取消</button>
                    <button type="button" class="btn btn-primary confirm-del">确定</button>
                </div>
            </div>
        </div>
    </div>
{% endblock %}

{% block js %}
    <script>
        $(function () {
            // 1.点击撤单按钮,弹出确认撤单弹出框
            $('.order-rm-btn').click(function () {
                $('.del_error_msg').empty()
                var Rid = $(this).attr('row-id')
                $('#delModal').modal('show');
                // 点击确定向后端发送ajax请求,删除
                $('.confirm-del').click(function () {
                    $.ajax({
                        url: '{% url "order_rm" %}',
                        type: 'get',
                        data: {rid: Rid},
                        dataType: 'json',
                        success: function (res) {
                            if (res.status) {
                                location.href = res.data;
                            } else {
                                $('.del_error_msg').text(res.detail)
                            }
                        }
                    })
                })
            })
        })
    </script>
{% endblock %}

10.2 下单页面

效果图:

image-20230719135556357

1.视图逻辑代码

下单需要涉及到多张表的操作,需要用到事务和锁,并且借助了一点爬虫代码(爬取视图url的原播放量)

并且下单页面涉及到价格的提示,需要根据价格策略表,生成价格策略提示

def order_add(request):
    if request.method == 'GET':
        form_obj = OrderAddForm()
        return render(request,'form.html',locals())

    form_obj = OrderAddForm(request.POST)
    if not form_obj.is_valid():
        return render(request, 'form.html', locals())

    # 校验通过之后的逻辑
    url = form_obj.cleaned_data['url']
    count = form_obj.cleaned_data['count']
    # 1.爬取原视频的播放量
    status,old_view_count = get_old_view_count(url)
    if not status:
        form_obj.add_error('url','视频原播放量获取失败,请重试')
        return render(request, 'form.html', locals())

    # 2.计算价格
    try:
        with transaction.atomic():
            for item in form_obj.price_list:
                if count >= item[0]:
                    price = count * item[1]
                    break
            customer_obj = models.Customer.objects.filter(active=1,id=request.user.id).select_for_update().first()
            percent = customer_obj.level.percent
            real_price = price * percent /100
            balance = customer_obj.balance

            # 3.判断用户账号余额还有没有那么多钱,没有则报错提示
            if real_price > balance:
                form_obj.add_error('count','当前余额{},请联系管理员充值'.format(balance))
                return render(request, 'form.html', locals())

            # 4.生成订单号
            while True:
                random_str = str(random.randint(1000, 9999))
                oid = datetime.now().strftime('%Y%m%d%H%M%S%f') + random_str
                is_exist = models.TransactionRecord.objects.filter(active=1, order_oid=oid).exists()
                if is_exist:
                    continue
                break

            # 5.生成订单
            form_obj.instance.oid = oid
            form_obj.instance.price = price
            form_obj.instance.real_price = real_price
            form_obj.instance.old_view_count = old_view_count
            form_obj.instance.customer = customer_obj
            form_obj.save()

            # 6.生成交易记录
            models.TransactionRecord.objects.create(
                charge_type=1,
                order_oid=oid,
                amount=real_price,
                customer=customer_obj,
            )

            # 7.扣除账户余额
            customer_obj.balance = customer_obj.balance - real_price
            customer_obj.save()

            # 8.写入任务队列
            conn = get_redis_connection('default')
            conn.lpush(settings.REDIS_QUEUE_NAME,oid)
    except Exception as e:
        form_obj.add_error('count','操作失败,请重试')
        return render(request, 'form.html', locals())
    # 9.操作完成,跳转回订单列表页面
    return redirect(reverse('order_list'))

2.用到的form组件

class OrderAddForm(forms.ModelForm):
    class Meta:
        model = models.Order
        fields = ['url','count']
        widgets = {
            'url':forms.TextInput(attrs={'class':'form-control','placeholder':'请输入视频地址'}),
            'count':forms.TextInput(attrs={'class':'form-control','placeholder':'请输入数量'})
        }

    def __init__(self,*args,**kwargs):
        super().__init__(*args,**kwargs)
        # 数量的旁边我们需要有一个提示,展示我们的价格策略
        # 比如返回这样一段提示:(>=1000,¥0.1/条,>=2000,...)
        text_list = []
        self.price_list = [] # 这个的设计是为了方便我们在视图中计算价格的
        queryset = models.PricePolicy.objects.filter(active=1).order_by('count')
        for obj in queryset:
            per_price = round(obj.price / obj.count,3)
            text = '>={},¥{}/条'.format(obj.count,per_price)
            text_list.append(text)
            self.price_list.append((obj.count,per_price))

        help_text = ','.join(text_list)
        self.fields['count'].help_text = help_text
        self.price_list.reverse()

    def clean_count(self):
        count = self.cleaned_data['count']
        min_count = models.PricePolicy.objects.filter(active=1).order_by('count').first().count
        if count < min_count:
            raise ValidationError('最小下单数量{}'.format(min_count))
        return count

3.前端代码

略,使用的公共模板form.html

10.3 撤单功能

对于待执行状态的订单,用户点击撤单按钮,可以进行撤单

1.视图逻辑代码

def order_rm(request):
    res = BasicResponse()
    rid = request.GET.get('rid')
    order_obj = models.Order.objects.filter(active=1,id=rid,status=1).first()
    if not order_obj:
        res.detail = '该订单不可撤单'
        return JsonResponse(res.detail)

    # 撤单涉及的业务逻辑
    real_price = order_obj.real_price
    try:
        with transaction.atomic():
            # 1.修改当前订单的状态为已撤单
            order_obj.status = 4
            order_obj.save()


            # 2.创建该订单的交易记录为撤单
            oid = order_obj.oid
            models.TransactionRecord.objects.create(
                charge_type=4,
                order_oid=oid,
                amount=real_price,
                customer_id=request.user.id,
            )

            # 3.退还该单金额到账户余额
            models.Customer.objects.filter(active=1,id=request.user.id).update(balance = F('balance')+real_price)
    except Exception as e:
        res.detail = '撤单失败,{}'.format(str(e))
        return JsonResponse(res.dict)
    # 4.撤单成功跳转到当前订单列表页面
    res.status = True
    res.data = '/order/list/'
    return JsonResponse(res.dict)

2.前端代码见上

11.分页、搜索、组合筛选功能的添加

11.1 分页功能

"""
如果想要以后使用分页,需要以下两个步骤:
在视图函数:
def customer_list(request):
    # 这里是获取搜索form表单提交的搜索关键字
    keyword = request.GET.get('keyword','').strip()
    # 使用Q对象进行或查询
    con = Q()
    if keyword:
        con.connector = 'OR'
        con.children.append(('username__contains', keyword))
        con.children.append(('mobile__contains', keyword))
        con.children.append(('level__title__contains', keyword))
    # 查询数据库,多了一个>filter是进行搜索查询
    queryset = models.Customer.objects.filter(con).filter(active=1).select_related('level')
    # 分页数据展示,引入这个定义好的类就行,传入request和查询出来的queryset
    pager = Pagination(request, queryset)
    # 返回给前端页面的数据,直接locals()即可
    return render(request,'customer_list.html',locals())

在页面上:
    展示数据
    {% for row in pager.queryset %}
        {{row.id}}
    {% endfor %}

    在数据下面添加分页,这个样式pagination是借用的bootstrap3的css样式
    <ul class="pagination">
        {{ pager.html }}
    </ul>
"""
import copy
from django.utils.safestring import mark_safe


class Pagination(object):
    """ 分页 """

    def __init__(self, request, query_set, per_page_count=10):

        self.query_dict = copy.deepcopy(request.GET)
        self.query_dict._mutable = True

        self.query_set = query_set
        total_count = query_set.count()
        self.total_count = total_count
        # 计算出总共有多少页面
        self.total_page, div = divmod(total_count, per_page_count)
        if div:
            self.total_page += 1

        page = request.GET.get('page')
        if not page:
            page = 1
        else:
            if not page.isdecimal():
                page = 1
            else:
                page = int(page)
                if page <= 0:
                    page = 1
                else:
                    if page > self.total_page:
                        page = self.total_page

        self.page = page
        self.per_page_count = per_page_count

        self.start = (page - 1) * per_page_count
        self.end = page * per_page_count

    def html(self):
        pager_list = []
        if not self.total_page:
            return ""

        # 总页码小于11
        if self.total_page <= 11:
            start_page = 1
            end_page = self.total_page
        else:
            # 总页码比较多
            # 判断当前页 <=6: 1~11
            if self.page <= 6:
                start_page = 1
                end_page = 11
            else:
                if (self.page + 5) > self.total_page:
                    start_page = self.total_page - 10
                    end_page = self.total_page
                else:
                    start_page = self.page - 5
                    end_page = self.page + 5

        self.query_dict.setlist('page', [1])
        pager_list.append('<li><a href="?{}">首页</a></li>'.format(self.query_dict.urlencode()))

        if self.page > 1:
            self.query_dict.setlist('page', [self.page - 1])
            pager_list.append('<li><a href="?{}">上一页</a></li>'.format(self.query_dict.urlencode()))

        for i in range(start_page, end_page + 1):
            self.query_dict.setlist('page', [i])
            if i == self.page:
                item = '<li class="active"><a href="?{}">{}</a></li>'.format(self.query_dict.urlencode(), i)
            else:
                item = '<li><a href="?{}">{}</a></li>'.format(self.query_dict.urlencode(), i)
            pager_list.append(item)

        if self.page < self.total_page:
            self.query_dict.setlist('page', [self.page + 1])
            pager_list.append('<li><a href="?{}">下一页</a></li>'.format(self.query_dict.urlencode()))

        self.query_dict.setlist('page', [self.total_page])
        pager_list.append('<li><a href="?{}">尾页</a></li>'.format(self.query_dict.urlencode()))

        pager_list.append('<li class="disabled"><a>数据{}条{}页</a></li>'.format(self.total_count, self.total_page))
        pager_string = mark_safe("".join(pager_list))
        return pager_string

    def queryset(self):
        if self.total_count:
            return self.query_set[self.start:self.end]
        return self.query_set

11.2 搜索功能

  • 前端页面展示搜索框
            <div style="float:right;">
                <form class="form-inline" method="get">
                    <div class="form-group">
                        <input name="keyword" type="text" class="form-control" placeholder="请输入关键字"
                               value="{{ keyword }}">
                    </div>
                    <button type="submit" class="btn btn-default">
                        <span class="glyphicon glyphicon-search"></span>
                    </button>
                </form>
            </div>
  • 后端视图逻辑代码
  # 这里是获取搜索form表单提交的搜索关键字
    keyword = request.GET.get('keyword','').strip()
    # 使用Q对象进行或查询
    con = Q()
    if keyword:
        con.connector = 'OR'
        con.children.append(('username__contains', keyword)) # 添加按xxx搜索条件
        con.children.append(('mobile__contains', keyword))
        con.children.append(('level__title__contains', keyword))
    # 查询数据库,返回的就是符合搜索条件的数据
    customer_queryset = models.Customer.objects.filter(con).filter(active=1).select_related('level')

11.3 组合筛选功能

1.把以下三个文件放到项目的utils文件夹、css文件和templates文件夹内,分别是group.py、search-group.css和search_group.html,还需要借助bootstrap3

  • group.py

    #!/usr/bin/env python
    # -*- coding:utf-8 -*-
    from django.db.models import ForeignKey, ManyToManyField
    
    
    class SearchGroupRow(object):
        def __init__(self, title, queryset_or_tuple, option, query_dict):
            """
            :param title: 组合搜索的列名称
            :param queryset_or_tuple: 组合搜索关联获取到的数据
            :param option: 配置
            :param query_dict: request.GET
            """
            self.title = title
            self.queryset_or_tuple = queryset_or_tuple
            self.option = option
            self.query_dict = query_dict
    
        def __iter__(self):
            yield '<div class="whole">'
            yield self.title
            yield '</div>'
            yield '<div class="others">'
            total_query_dict = self.query_dict.copy()
            total_query_dict._mutable = True
    
            origin_value_list = self.query_dict.getlist(self.option.field)
            if not origin_value_list:
                yield "<a class='active' href='?%s'>全部</a>" % total_query_dict.urlencode()
            else:
                total_query_dict.pop(self.option.field)
                yield "<a href='?%s'>全部</a>" % total_query_dict.urlencode()
    
            for item in self.queryset_or_tuple:
                text = self.option.get_text(item)
                value = str(self.option.get_value(item))
                query_dict = self.query_dict.copy()
                query_dict._mutable = True
    
                if not self.option.is_multi:
                    query_dict[self.option.field] = value
                    if value in origin_value_list:
                        query_dict.pop(self.option.field)
                        yield "<a class='active' href='?%s'>%s</a>" % (query_dict.urlencode(), text)
                    else:
                        yield "<a href='?%s'>%s</a>" % (query_dict.urlencode(), text)
                else:
                    # {'gender':['1','2']}
                    multi_value_list = query_dict.getlist(self.option.field)
                    if value in multi_value_list:
                        multi_value_list.remove(value)
                        query_dict.setlist(self.option.field, multi_value_list)
                        yield "<a class='active' href='?%s'>%s</a>" % (query_dict.urlencode(), text)
                    else:
                        multi_value_list.append(value)
                        query_dict.setlist(self.option.field, multi_value_list)
                        yield "<a href='?%s'>%s</a>" % (query_dict.urlencode(), text)
    
            yield '</div>'
    
    
    class Option(object):
        def __init__(self, field, is_condition=True, is_multi=False, db_condition=None, text_func=None, value_func=None):
            """
            :param field: 组合搜索关联的字段
            :param is_multi: 是否支持多选
            :param db_condition: 数据库关联查询时的条件
            :param text_func: 此函数用于显示组合搜索按钮页面文本
            :param value_func: 此函数用于显示组合搜索按钮值
            """
            self.field = field
            self.is_condition = is_condition
            self.is_multi = is_multi
            if not db_condition:
                db_condition = {}
            self.db_condition = db_condition
            self.text_func = text_func
            self.value_func = value_func
    
            self.is_choice = False
    
        def get_db_condition(self, request, *args, **kwargs):
            return self.db_condition
    
        def get_queryset_or_tuple(self, model_class, request, *args, **kwargs):
            """
            根据字段去获取数据库关联的数据
            :return:
            """
            # 根据gender或depart字符串,去自己对应的Model类中找到 字段对象
            field_object = model_class._meta.get_field(self.field)
            title = field_object.verbose_name
            # 获取关联数据
            if isinstance(field_object, ForeignKey) or isinstance(field_object, ManyToManyField):
                # FK和M2M,应该去获取其关联表中的数据: QuerySet
                db_condition = self.get_db_condition(request, *args, **kwargs)
                return SearchGroupRow(title,
                                      field_object.remote_field.model.objects.filter(**db_condition),
                                      self,
                                      request.GET)
            else:
                # 获取choice中的数据:元组
                self.is_choice = True
                return SearchGroupRow(title, field_object.choices, self, request.GET)
    
        def get_text(self, field_object):
            """
            获取文本函数
            :param field_object:
            :return:
            """
            if self.text_func:
                return self.text_func(field_object)
    
            if self.is_choice:
                return field_object[1]
    
            return str(field_object)
    
        def get_value(self, field_object):
            if self.value_func:
                return self.value_func(field_object)
    
            if self.is_choice:
                return field_object[0]
    
            return field_object.pk
    
        def get_search_condition(self, request):
            if not self.is_condition:
                return None
            if self.is_multi:
                values_list = request.GET.getlist(self.field)  # tags=[1,2]
                if not values_list:
                    return None
                return '%s__in' % self.field, values_list
            else:
                value = request.GET.get(self.field)  # tags=[1,2]
                if not value:
                    return None
                return self.field, value
    
    
    class NbSearchGroup(object):
        def __init__(self, request, model_class, *options):
            self.request = request
            self.model_class = model_class
            self.options = options
    
        def get_row_list(self):
            row_list = []
            for option_object in self.options:
                row = option_object.get_queryset_or_tuple(self.model_class, self.request)
                row_list.append(row)
            return row_list
    
        @property
        def get_condition(self):
            """
            获取组合搜索的条件
            :param request:
            :return:
            """
            condition = {}
            # ?depart=1&gender=2&page=123&q=999
            for option in self.options:
                key_and_value = option.get_search_condition(self.request)
                if not key_and_value:
                    continue
                key, value = key_and_value
                condition[key] = value
    
            return condition
    
    
  • search-group.css

    .search-group {
        padding: 5px 10px;
    }
    
    .search-group .row .whole {
        width: 60px;
        float: left;
        display: inline-block;
        padding: 5px 0 5px 8px;
        margin: 3px;
        font-weight: bold;
        text-align: right;
    
    }
    
    .search-group .row .others {
        padding-left: 80px;
    }
    
    .search-group .row a {
        display: inline-block;
        padding: 5px 8px;
        margin: 3px;
        border: 1px solid #d4d4d4;
    
    }
    
    .search-group .row a {
        display: inline-block;
        padding: 5px 8px;
        margin: 3px;
        border: 1px solid #d4d4d4;
    }
    
    .search-group a.active {
        color: #fff;
        background-color: #337ab7;
        border-color: #2e6da4;
    }
    
  • search_group.html

    {% if search_group.get_row_list %}
        <div class="panel panel-default">
            <div class="panel-heading">
                <i class="fa fa-filter" aria-hidden="true"></i> 快速筛选
            </div>
            <div class="panel-body">
                <div class="search-group">
                    {% for row in search_group.get_row_list %}
                        <div class="row">
                            {% for obj in row %}
                                {{ obj|safe }}
                            {% endfor %}
                        </div>
                    {% endfor %}
                </div>
            </div>
        </div>
    {% endif %}
    

2.在djang视图函数和模板层中使用

# 导入
from utils.group import Option,NbSearchGroup

image-20230719180057784

image-20230719180143993

image-20230719180303128

image-20230720083501132

12 功能小优化

12.1 权限优化--按钮的控制

权限判断-按钮控制功能 - 等日落 - 博客园 (cnblogs.com)

12.2 页面跳转携带当前页参数

关于如何让页面跳转时携带当前页面的参数 - 等日落 - 博客园 (cnblogs.com)

12.3 交易类型样式的优化

image-20230720091951113

对于交易类型,我们想要让每种交易类型有一种样式;此时,由于每种交易类型在数据库中代表一个值,我们可以对这些值做一个映射,每个的值是一个对应bootstrap中的样式

class TransactionRecord(BasicModel):

    charge_type_choice = (
        (1,'创建订单'),
        (2,'充值'),
        (3,'扣款'),
        (4,'撤单'),
        (5,'删除订单')
    )

    charge_type_mapping = {
        1:'success',
        2:'info',
        3:'primary',
        4:'danger',
        5:'default'
    }

    charge_type = models.SmallIntegerField(verbose_name='类型',choices=charge_type_choice)

然后在模板层自定义一个filter,根据不同的交易类型,去找到该交易类型定义的mapping

 <td>
      <span class="btn btn-xs btn-{{ obj.charge_type|charge_type_color }}">{{ obj.get_charge_type_display }}</span>
</td>

自定义的filter

from django import template
from web import models

register = template.Library()

@register.filter
def charge_type_color(num):
    return models.TransactionRecord.charge_type_mapping[num]


@register.filter
def status_color(num):
    return models.Order.status_mapping[num]

完成效果图:

image-20230720094608078

posted @ 2023-08-11 15:02  等日落  阅读(44)  评论(0编辑  收藏  举报