最全最细的订单交易系统复盘-cnblog
最全最细的订单交易系统复盘
0.项目技术栈
项目采用前后端不分离的模式开发
- 后端:python的django框架
- 前端:借用django里面的模板层语法、
- 数据库:mysql
- 缓存:redis
1.项目环境搭建
1.1 创建django项目
- 创建纯python项目虚拟环境
- pycharm终端安装django3.2版本
pip install django==3.2
- 终端执行创建django项目命令
django-admin startproject order_transaction_system2 .
注意:
- 创建django项目命令后面加个. 表示创建django项目文件夹的路径是在当前执行命令的目录
- django项目的文件名必须和刚刚上面创建的python项目名同名,否则报错,这是因为后面加了个点的原因
- 如果不加. django项目的文件名可以随意
- 启动django服务
# pycharm终端执行以下命令
python manage.py runserver
# 这个命令后面还可以指定运行的ip和端口,详细用法,可以百度!
上述启动方法,每次启动太麻烦,我们可以在pycharm中配置快速启动快捷键
- 配置pycharm中快速启动django的快捷键
1.2 创建app
-
终端执行创建app命令
python manage.py startapp web
-
settings注册该app
注:使用app之前一定要先注册app,否则会报错
1.3 修改settings里的一些配置
-
注释掉,我们项目不需要用到的app和中间件,这样可以减轻django的"重量"
-
配置项目需要用到的静态文件资源存放路径
-
配置我们需要连接的mysql数据库
-
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": "密码", } } }
-
-
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数据库,还需要先一些配置
-
安装pymysql
pip install pymysql
-
在django项目的任意
__init__
文件内书写以下两行代码import pymysql pymysql.install_as_MySQLdb()
-
执行迁移到数据库命令
# 先执行 python manage.py makemigrations # 再执行 python manage.py migrate
-
之后,我们数据库里就有对应的表了,可以用mysql数据库管理工具,比如navicat或者django自带的管理工具查看数据库了
3.登录
先做登录相关功能
包括:用户名密码登录和邮箱验证码登录
效果图:
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 视图函数逻辑
-
app视图结构的介绍
- 改动前
- 改动后
-
登录视图函数逻辑代码
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 密码登录涉及的补充知识点
-
django中forms组件的使用
-
md5加密加盐
-
session的使用(作用是为了后面的登录和权限校验进行校验的)
-
前端知识补充
-
如何让一个盒子的边框变成圆角
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的门户,每次请求都要先经过中间件
-
在utils里新建一个py文件,用来编写自定义的中间件
-
在新建的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
-
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">×</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.客户管理
基本同级别管理略
效果图:
后期会加搜索框和筛选功能
8.价格管理
基本同级别管理略
效果图:
9.交易记录
基本同级别管理略
效果图:
9.1 充值扣款功能
当点击某个客户的交易记录,会跳转该客户的交易记录列表页面,在该页面可以,点击扣款或者充值按钮,给该客户充值或者扣款
效果图:
充值和扣款需要涉及两张表,所以我们需要用到事务和锁的概念
视图逻辑代码:
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">×</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 订单列表的展示
效果图:
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">×</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 下单页面
效果图:
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
12 功能小优化
12.1 权限优化--按钮的控制
权限判断-按钮控制功能 - 等日落 - 博客园 (cnblogs.com)
12.2 页面跳转携带当前页参数
关于如何让页面跳转时携带当前页面的参数 - 等日落 - 博客园 (cnblogs.com)
12.3 交易类型样式的优化
对于交易类型,我们想要让每种交易类型有一种样式;此时,由于每种交易类型在数据库中代表一个值,我们可以对这些值做一个映射,每个的值是一个对应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]
完成效果图: