Fork me on GitHub

Django+Vue打造购物网站

环境搭建

python == 3.6
Django == 2.0

创建工程

1
django-admin startproject MxShop

配置setting.py文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
# 数据库
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.mysql',
'NAME': 'mxshop', #数据库名字
'USER': 'root', #账号
'PASSWORD': '123456', #密码
'HOST': '127.0.0.1', #IP
'PORT': '3306', #端口
#因为后面第三方登录时,要求引擎为INNODB
"OPTIONS":{"init_command":"SET default_storage_engine=INNODB;"}
}
}

# 使用中文
LANGUAGE_CODE = 'zh-hans'

TIME_ZONE = 'Asia/Shanghai'

USE_I18N = True

USE_L10N = True

USE_TZ = False


# 静态文件路径(须在根目录下创建static目录)

STATIC_URL = '/static/'
STATICFILES_DIRS = (
os.path.join(BASE_DIR, 'static'),
)

安装djangorestframework

1
2
3
pip install djangorestframework
pip install markdown # Markdown support for the browsable API.
pip install django-filter # Filtering support

安装其它的插件
mysql插件地址https://www.lfd.uci.edu/~gohlke/pythonlibs/#mysqlclint,自行下载

1
2
pip install mysqlclient-1.3.12-cp36-cp36m-win_amd64
pip install pillow

新建两个python package

  • extra_apps (扩展的源码包)
  • apps (放所有app)

新建两个文件夹

  • media (保存图片)
  • db_tools (数据库相关)

把extra_apps和apps添加到sys.path中

1
2
3
4
5
6
7
8
# settings.py

import sys

BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
sys.path.insert(0,BASE_DIR)
sys.path.insert(0,os.path.join(BASE_DIR, 'apps'))
sys.path.insert(0,os.path.join(BASE_DIR, 'extra_apps'))

models 设计

首先安装xadmin和DjangoUeditor
安装方法参考之前的在线教育系统

1
2
3
4
5
6
7
python manage.py startapp users

python manage.py startapp goods

python manage.py startapp trade

python manage.py startapp user_operation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'rest_framework',
'users',
'user_operation',
'trade',
'goods',
'xadmin',
'DjangoUeditor',
'crispy_forms',
]

users models设计

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
from datetime import datetime

from django.contrib.auth.models import AbstractUser
from django.db import models


# Create your models here.
class UserProfile(AbstractUser):
"""
用户信息
"""
GENDER_CHOICES = (
("male", "男"),
("female", "女")
)
# 用户用手机注册,所以姓名,生日和邮箱可以为空
name = models.CharField(verbose_name="姓名",max_length=30, null=True, blank=True)
birthday = models.DateField(verbose_name="出生年月",null=True, blank=True)
gender = models.CharField(verbose_name="性别",max_length=6, choices=GENDER_CHOICES, default="female")
mobile = models.CharField(verbose_name="电话",max_length=11)
email = models.EmailField(verbose_name="邮箱",max_length=100, null=True, blank=True)

class Meta:
verbose_name = "用户信息"
verbose_name_plural = verbose_name

def __str__(self):
return self.username


class VerifyCode(models.Model):
"""
验证码
"""
code = models.CharField(verbose_name="验证码",max_length=10)
mobile = models.CharField(verbose_name="电话",max_length=11)
add_time = models.DateTimeField(verbose_name="添加时间",default=datetime.now)

class Meta:
verbose_name = "短信验证"
verbose_name_plural = verbose_name

def __str__(self):
return self.code

trade models设计

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

from django.contrib.auth import get_user_model
from django.db import models

# Create your models here.
from goods.models import Goods

User = get_user_model()


class ShoppingCart(models.Model):
"""
购物车
"""
user = models.ForeignKey(User, verbose_name=u"用户", on_delete=models.CASCADE)
goods = models.ForeignKey(Goods, verbose_name=u"商品", on_delete=models.CASCADE)
nums = models.IntegerField(default=0, verbose_name="购买数量")

add_time = models.DateTimeField(default=datetime.now, verbose_name=u"添加时间")

class Meta:
verbose_name = '购物车'
verbose_name_plural = verbose_name
unique_together = ("user", "goods")

def __str__(self):
return "%s(%d)".format(self.goods.name, self.nums)


class OrderInfo(models.Model):
"""
订单
"""
ORDER_STATUS = (
("TRADE_SUCCESS", "成功"),
("TRADE_CLOSED", "超时关闭"),
("WAIT_BUYER_PAY", "交易创建"),
("TRADE_FINISHED", "交易结束"),
("paying", "待支付"),
)

user = models.ForeignKey(User, verbose_name="用户", on_delete=models.CASCADE)
order_sn = models.CharField(max_length=30, null=True, blank=True, unique=True, verbose_name="订单号")
trade_no = models.CharField(max_length=100, unique=True, null=True, blank=True, verbose_name=u"交易号")
pay_status = models.CharField(choices=ORDER_STATUS, default="paying", max_length=30, verbose_name="订单状态")
post_script = models.CharField(max_length=200, verbose_name="订单留言")
order_mount = models.FloatField(default=0.0, verbose_name="订单金额")
pay_time = models.DateTimeField(null=True, blank=True, verbose_name="支付时间")

# 用户信息
address = models.CharField(max_length=100, default="", verbose_name="收货地址")
signer_name = models.CharField(max_length=20, default="", verbose_name="签收人")
singer_mobile = models.CharField(max_length=11, verbose_name="联系电话")

add_time = models.DateTimeField(default=datetime.now, verbose_name="添加时间")

class Meta:
verbose_name = u"订单"
verbose_name_plural = verbose_name

def __str__(self):
return str(self.order_sn)


class OrderGoods(models.Model):
"""
订单的商品详情
"""
order = models.ForeignKey(OrderInfo, verbose_name="订单信息", related_name="goods", on_delete=models.CASCADE)
goods = models.ForeignKey(Goods, verbose_name="商品", on_delete=models.CASCADE)
goods_num = models.IntegerField(default=0, verbose_name="商品数量")
add_time = models.DateTimeField(default=datetime.now, verbose_name="添加时间")

class Meta:
verbose_name = "订单商品"
verbose_name_plural = verbose_name

def __str__(self):
return str(self.order.order_sn)

goods models 设计

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
from datetime import datetime

from django.db import models


# Create your models here.
class GoodsCategory(models.Model):
"""
商品类别
"""
CATEGORY_TYPE = (
(1, "一级类目"),
(2, "二级类目"),
(3, "三级类目"),
)

name = models.CharField(default="", max_length=30, verbose_name="类别名", help_text="类别名")
code = models.CharField(default="", max_length=30, verbose_name="类别code", help_text="类别code")
desc = models.TextField(default="", verbose_name="类别描述", help_text="类别描述")
category_type = models.IntegerField(choices=CATEGORY_TYPE, verbose_name="类目级别", help_text="类目级别")
parent_category = models.ForeignKey("self", null=True, blank=True, verbose_name="父类目级别", help_text="父目录",
related_name="sub_cat",on_delete=models.CASCADE )
is_tab = models.BooleanField(default=False, verbose_name="是否导航", help_text="是否导航")
add_time = models.DateTimeField(default=datetime.now, verbose_name="添加时间")

class Meta:
verbose_name = "商品类别"
verbose_name_plural = verbose_name

def __str__(self):
return self.name


class GoodsCategoryBrand(models.Model):
"""
品牌名
"""
category = models.ForeignKey(GoodsCategory, related_name='brands', null=True, blank=True, verbose_name="商品类目", on_delete=models.CASCADE)
name = models.CharField(default="", max_length=30, verbose_name="品牌名", help_text="品牌名")
desc = models.TextField(default="", max_length=200, verbose_name="品牌描述", help_text="品牌描述")
image = models.ImageField(max_length=200, upload_to="brands/")
add_time = models.DateTimeField(default=datetime.now, verbose_name="添加时间")

class Meta:
verbose_name = "品牌"
verbose_name_plural = verbose_name

def __str__(self):
return self.name


class Goods(models.Model):
"""
商品
"""
category = models.ForeignKey(GoodsCategory, verbose_name="商品类目", on_delete=models.CASCADE)
goods_sn = models.CharField(max_length=50, default="", verbose_name="商品唯一货号")
name = models.CharField(max_length=100, verbose_name="商品名")
click_num = models.IntegerField(default=0, verbose_name="点击数")
sold_num = models.IntegerField(default=0, verbose_name="商品销售量")
fav_num = models.IntegerField(default=0, verbose_name="收藏数")
goods_num = models.IntegerField(default=0, verbose_name="库存数")
market_price = models.FloatField(default=0, verbose_name="市场价格")
shop_price = models.FloatField(default=0, verbose_name="本店价格")
goods_brief = models.TextField(max_length=500, verbose_name="商品简短描述")
goods_desc = UEditorField(verbose_name=u"内容", imagePath="goods/images/", width=1000, height=300,
filePath="goods/files/", default='')
ship_free = models.BooleanField(default=True, verbose_name="是否承担运费")
goods_front_image = models.ImageField(upload_to="goods/images/", null=True, blank=True, verbose_name="封面图")
is_new = models.BooleanField(default=False, verbose_name="是否新品")
is_hot = models.BooleanField(default=False, verbose_name="是否热销")
add_time = models.DateTimeField(default=datetime.now, verbose_name="添加时间")

class Meta:
verbose_name = '商品'
verbose_name_plural = verbose_name

def __str__(self):
return self.name


class IndexAd(models.Model):
category = models.ForeignKey(GoodsCategory, related_name='category',verbose_name="商品类目", on_delete=models.CASCADE)
goods =models.ForeignKey(Goods, related_name='goods', on_delete=models.CASCADE)

class Meta:
verbose_name = '首页商品类别广告'
verbose_name_plural = verbose_name

def __str__(self):
return self.goods.name


class GoodsImage(models.Model):
"""
商品轮播图
"""
goods = models.ForeignKey(Goods, verbose_name="商品", related_name="images", on_delete=models.CASCADE)
image = models.ImageField(upload_to="", verbose_name="图片", null=True, blank=True)
add_time = models.DateTimeField(default=datetime.now, verbose_name="添加时间")

class Meta:
verbose_name = '商品图片'
verbose_name_plural = verbose_name

def __str__(self):
return self.goods.name


class Banner(models.Model):
"""
轮播的商品
"""
goods = models.ForeignKey(Goods, verbose_name="商品", on_delete=models.CASCADE)
image = models.ImageField(upload_to='banner', verbose_name="轮播图片")
index = models.IntegerField(default=0, verbose_name="轮播顺序")
add_time = models.DateTimeField(default=datetime.now, verbose_name="添加时间")

class Meta:
verbose_name = '轮播商品'
verbose_name_plural = verbose_name

def __str__(self):
return self.goods.name


class HotSearchWords(models.Model):
"""
热搜词
"""
keywords = models.CharField(default="", max_length=20, verbose_name="热搜词")
index = models.IntegerField(default=0, verbose_name="排序")
add_time = models.DateTimeField(default=datetime.now, verbose_name="添加时间")

class Meta:
verbose_name = '热搜词'
verbose_name_plural = verbose_name

def __str__(self):
return self.keywords

user_operation models设计

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

from django.contrib.auth import get_user_model
from django.db import models

# Create your models here.
from goods.models import Goods

User = get_user_model()


class UserFav(models.Model):
"""
用户收藏
"""
user = models.ForeignKey(User, verbose_name="用户", on_delete=models.CASCADE)
goods = models.ForeignKey(Goods, verbose_name="商品", help_text="商品id", on_delete=models.CASCADE)
add_time = models.DateTimeField(default=datetime.now, verbose_name=u"添加时间")

class Meta:
verbose_name = '用户收藏'
verbose_name_plural = verbose_name
unique_together = ("user", "goods")

def __str__(self):
return self.user.username


class UserLeavingMessage(models.Model):
"""
用户留言
"""
MESSAGE_CHOICES = (
(1, "留言"),
(2, "投诉"),
(3, "询问"),
(4, "售后"),
(5, "求购")
)
user = models.ForeignKey(User, verbose_name="用户", on_delete=models.CASCADE)
message_type = models.IntegerField(default=1, choices=MESSAGE_CHOICES, verbose_name="留言类型",
help_text=u"留言类型: 1(留言),2(投诉),3(询问),4(售后),5(求购)")
subject = models.CharField(max_length=100, default="", verbose_name="主题")
message = models.TextField(default="", verbose_name="留言内容", help_text="留言内容")
file = models.FileField(upload_to="message/images/", verbose_name="上传的文件", help_text="上传的文件")
add_time = models.DateTimeField(default=datetime.now, verbose_name="添加时间")

class Meta:
verbose_name = "用户留言"
verbose_name_plural = verbose_name

def __str__(self):
return self.subject


class UserAddress(models.Model):
"""
用户收货地址
"""
user = models.ForeignKey(User, verbose_name="用户", on_delete=models.CASCADE)
province = models.CharField(max_length=100, default="", verbose_name="省份")
city = models.CharField(max_length=100, default="", verbose_name="城市")
district = models.CharField(max_length=100, default="", verbose_name="区域")
address = models.CharField(max_length=100, default="", verbose_name="详细地址")
signer_name = models.CharField(max_length=100, default="", verbose_name="签收人")
signer_mobile = models.CharField(max_length=11, default="", verbose_name="电话")
add_time = models.DateTimeField(default=datetime.now, verbose_name="添加时间")

class Meta:
verbose_name = "收货地址"
verbose_name_plural = verbose_name

def __str__(self):
return self.address

将models映射到数据库中

1
2
python manage.py makemigrations
python manage.py migrate

配置后台管理

xadmin直接使用之前的在线教育的那个就可以了

users/adminx.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# @Time : 2018/9/19 下午 01:15
# @Author : gao
# @File : adminx.py
import xadmin
from users.models import VerifyCode
from xadmin import views


class BaseSetting(object):
enable_themes = True
use_bootswatch = True


class GlobalSettings(object):
site_title = "慕学生鲜后台"
site_footer = "mxshop"
menu_style = "accordion"


class VerifyCodeAdmin(object):
list_display = ['code', 'mobile', "add_time"]


xadmin.site.register(VerifyCode, VerifyCodeAdmin)
xadmin.site.register(views.BaseAdminView, BaseSetting)
xadmin.site.register(views.CommAdminView, GlobalSettings)

goods/adminx.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# @Time : 2018/9/19 下午 01:15
# @Author : gao
# @File : adminx.py
import xadmin
from goods.models import Goods, GoodsCategory, Banner, GoodsCategoryBrand, HotSearchWords, IndexAd, GoodsImage


class GoodsAdmin(object):
list_display = ["name", "click_num", "sold_num", "fav_num", "goods_num", "market_price",
"shop_price", "goods_brief", "goods_desc", "is_new", "is_hot", "add_time"]
search_fields = ['name', ]
list_editable = ["is_hot", ]
list_filter = ["name", "click_num", "sold_num", "fav_num", "goods_num", "market_price",
"shop_price", "is_new", "is_hot", "add_time", "category__name"]
style_fields = {"goods_desc": "ueditor"}

class GoodsImagesInline(object):
model = GoodsImage
exclude = ["add_time"]
extra = 1
style = 'tab'

inlines = [GoodsImagesInline]


class GoodsCategoryAdmin(object):
list_display = ["name", "category_type", "parent_category", "add_time"]
list_filter = ["category_type", "parent_category", "name"]
search_fields = ['name', ]


class GoodsBrandAdmin(object):
list_display = ["category", "image", "name", "desc"]

def get_context(self):
context = super(GoodsBrandAdmin, self).get_context()
if 'form' in context:
context['form'].fields['category'].queryset = GoodsCategory.objects.filter(category_type=1)
return context


class BannerGoodsAdmin(object):
list_display = ["goods", "image", "index"]


class HotSearchAdmin(object):
list_display = ["keywords", "index", "add_time"]


class IndexAdAdmin(object):
list_display = ["category", "goods"]


xadmin.site.register(Goods, GoodsAdmin)
xadmin.site.register(GoodsCategory, GoodsCategoryAdmin)
xadmin.site.register(Banner, BannerGoodsAdmin)
xadmin.site.register(GoodsCategoryBrand, GoodsBrandAdmin)

xadmin.site.register(HotSearchWords, HotSearchAdmin)
xadmin.site.register(IndexAd, IndexAdAdmin)

trade/adminx.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# @Time : 2018/9/19 下午 01:15
# @Author : gao
# @File : adminx.py
import xadmin
from goods.models import Goods, GoodsCategory, Banner, GoodsCategoryBrand, HotSearchWords, IndexAd, GoodsImage


class GoodsAdmin(object):
list_display = ["name", "click_num", "sold_num", "fav_num", "goods_num", "market_price",
"shop_price", "goods_brief", "goods_desc", "is_new", "is_hot", "add_time"]
search_fields = ['name', ]
list_editable = ["is_hot", ]
list_filter = ["name", "click_num", "sold_num", "fav_num", "goods_num", "market_price",
"shop_price", "is_new", "is_hot", "add_time", "category__name"]
style_fields = {"goods_desc": "ueditor"}

class GoodsImagesInline(object):
model = GoodsImage
exclude = ["add_time"]
extra = 1
style = 'tab'

inlines = [GoodsImagesInline]


class GoodsCategoryAdmin(object):
list_display = ["name", "category_type", "parent_category", "add_time"]
list_filter = ["category_type", "parent_category", "name"]
search_fields = ['name', ]


class GoodsBrandAdmin(object):
list_display = ["category", "image", "name", "desc"]

def get_context(self):
context = super(GoodsBrandAdmin, self).get_context()
if 'form' in context:
context['form'].fields['category'].queryset = GoodsCategory.objects.filter(category_type=1)
return context


class BannerGoodsAdmin(object):
list_display = ["goods", "image", "index"]


class HotSearchAdmin(object):
list_display = ["keywords", "index", "add_time"]


class IndexAdAdmin(object):
list_display = ["category", "goods"]


xadmin.site.register(Goods, GoodsAdmin)
xadmin.site.register(GoodsCategory, GoodsCategoryAdmin)
xadmin.site.register(Banner, BannerGoodsAdmin)
xadmin.site.register(GoodsCategoryBrand, GoodsBrandAdmin)

xadmin.site.register(HotSearchWords, HotSearchAdmin)
xadmin.site.register(IndexAd, IndexAdAdmin)

user_operation/adminx.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# @Time : 2018/9/19 下午 01:15
# @Author : gao
# @File : adminx.py
import xadmin
from goods.models import Goods, GoodsCategory, Banner, GoodsCategoryBrand, HotSearchWords, IndexAd, GoodsImage


class GoodsAdmin(object):
list_display = ["name", "click_num", "sold_num", "fav_num", "goods_num", "market_price",
"shop_price", "goods_brief", "goods_desc", "is_new", "is_hot", "add_time"]
search_fields = ['name', ]
list_editable = ["is_hot", ]
list_filter = ["name", "click_num", "sold_num", "fav_num", "goods_num", "market_price",
"shop_price", "is_new", "is_hot", "add_time", "category__name"]
style_fields = {"goods_desc": "ueditor"}

class GoodsImagesInline(object):
model = GoodsImage
exclude = ["add_time"]
extra = 1
style = 'tab'

inlines = [GoodsImagesInline]


class GoodsCategoryAdmin(object):
list_display = ["name", "category_type", "parent_category", "add_time"]
list_filter = ["category_type", "parent_category", "name"]
search_fields = ['name', ]


class GoodsBrandAdmin(object):
list_display = ["category", "image", "name", "desc"]

def get_context(self):
context = super(GoodsBrandAdmin, self).get_context()
if 'form' in context:
context['form'].fields['category'].queryset = GoodsCategory.objects.filter(category_type=1)
return context


class BannerGoodsAdmin(object):
list_display = ["goods", "image", "index"]


class HotSearchAdmin(object):
list_display = ["keywords", "index", "add_time"]


class IndexAdAdmin(object):
list_display = ["category", "goods"]


xadmin.site.register(Goods, GoodsAdmin)
xadmin.site.register(GoodsCategory, GoodsCategoryAdmin)
xadmin.site.register(Banner, BannerGoodsAdmin)
xadmin.site.register(GoodsCategoryBrand, GoodsBrandAdmin)

xadmin.site.register(HotSearchWords, HotSearchAdmin)
xadmin.site.register(IndexAd, IndexAdAdmin)

后台菜单变中文

两种方式,主要根据INSTALLED_APPS来决定

第一种

使用config来配置

1
'users.apps.UsersConfig',

users/apps.py

1
2
3
4
5
6
from django.apps import AppConfig


class UsersConfig(AppConfig):
name = 'users'
verbose_name = "用户管理"

第二种

1
'users',

users/apps.py

1
2
3
4
5
6
from django.apps import AppConfig


class UsersConfig(AppConfig):
name = 'users'
verbose_name = "用户管理"

users/__ini__.py

1
default_app_config = 'users.apps.UsersConfig'

具体使用哪种可自行决定,并修改其它三处

user_operation/apps.py

1
2
3
4
5
6
from django.apps import AppConfig


class UserOperationConfig(AppConfig):
name = 'user_operation'
verbose_name = "用户操作管理"

trade/apps.py

1
2
3
4
5
6
from django.apps import AppConfig


class TradeConfig(AppConfig):
name = 'trade'
verbose_name = "交易管理"

goods/apps.py

1
2
3
4
5
6
from django.apps import AppConfig


class GoodsConfig(AppConfig):
name = 'goods'
verbose_name = "商品"

导入数据

由于分类和商品很多,就写个脚本导入数据
单独使用django的model,批量导入数据
db_tools下新建文件夹data,将category_data.py和product_data.py拷贝到这里面
把图片都拷贝到media目录下

db_tools目录下新建文件 import_category_data.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# @Time : 2018/9/19 下午 04:01
# @Author : gao
# @File : import_category_data.py


# 独立使用django的model
import sys
import os

# 获取当前文件的路径(运行脚本)
pwd = os.path.dirname(os.path.realpath(__file__))
# 获取项目的根目录
sys.path.append(pwd + "../")
# 要想单独使用django的model,必须指定一个环境变量,会去settings配置找
# 参照manage.py里面就知道为什么这样设置了
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "MxShop.settings")

import django

django.setup()

from goods.models import GoodsCategory

from db_tools.data.category_data import row_data

# 一级类
for lev1_cat in row_data:
lev1_intance = GoodsCategory()
lev1_intance.code = lev1_cat["code"]
lev1_intance.name = lev1_cat["name"]
lev1_intance.category_type = 1
# 保存到数据库
lev1_intance.save()
# 二级类
for lev2_cat in lev1_cat["sub_categorys"]:
lev2_intance = GoodsCategory()
lev2_intance.code = lev2_cat["code"]
lev2_intance.name = lev2_cat["name"]
lev2_intance.category_type = 2
lev2_intance.parent_category = lev1_intance
lev2_intance.save()
# 三级类
for lev3_cat in lev2_cat["sub_categorys"]:
lev3_intance = GoodsCategory()
lev3_intance.code = lev3_cat["code"]
lev3_intance.name = lev3_cat["name"]
lev3_intance.category_type = 3
lev3_intance.parent_category = lev2_intance
lev3_intance.save()

db_tools目录下新建文件import_goods_data.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# @Time : 2018/9/19 下午 04:02
# @Author : gao
# @File : import_goods_data.py
import sys
import os

pwd = os.path.dirname(os.path.realpath(__file__))
sys.path.append(pwd+"../")
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "MxShop.settings")

import django
django.setup()

from goods.models import Goods, GoodsCategory, GoodsImage

from db_tools.data.product_data import row_data

for goods_detail in row_data:
goods = Goods()
goods.name = goods_detail["name"]
#前端中是“¥232”,数据库中是float类型,所以要替换掉
goods.market_price = float(int(goods_detail["market_price"].replace("¥", "").replace("元", "")))
goods.shop_price = float(int(goods_detail["sale_price"].replace("¥", "").replace("元", "")))
goods.goods_brief = goods_detail["desc"] if goods_detail["desc"] is not None else ""
goods.goods_desc = goods_detail["goods_desc"] if goods_detail["goods_desc"] is not None else ""
# 取第一张作为封面图
goods.goods_front_image = goods_detail["images"][0] if goods_detail["images"] else ""
#取最后一个
category_name = goods_detail["categorys"][-1]
# 取出当前子类对应的GoodsCategory对象,filter没有匹配的会返回空数组,不会抛异常.
category = GoodsCategory.objects.filter(name=category_name)
if category:
goods.category = category[0]
goods.save()

for goods_image in goods_detail["images"]:
goods_image_instance = GoodsImage()
goods_image_instance.image = goods_image
goods_image_instance.goods = goods
goods_image_instance.save()

运行这两个脚本文件,将数据添加到数据库中

配置media路径

settings.py

1
2
3
# 设置上传文件的路径
MEDIA_URL="/media/"
MEDIA_ROOT=os.path.join(BASE_DIR,"media")

urls.py

1
2
3
4
5
6
7
8
9
10
11
12
from django.views.static import serve

import xadmin
from django.urls import path, include

from MxShop.settings import MEDIA_ROOT

urlpatterns = [
path('admin/', xadmin.site.urls),
path('ueditor/', include('DjangoUeditor.urls')),
path('media/<path:path>', serve, {'document_root': MEDIA_ROOT}),
]

商品列表页

通过商品列表页面来学习drf

django的view实现商品列表页

在goods目录下新建一个views_base.py文件,用来区分drf的view和Dajngo自带的view的区别
利用Django的view实现返回json数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# @Time : 2018/9/20 下午 01:16
# @Author : gao
# @File : views_base.py


from django.views.generic.base import View

from goods.models import Goods


class GoodsListView(View):
def get(self, request):
# 通过django的view实现商品列表页
json_list = []
# 获取所有商品
goods = Goods.objects.all()
for good in goods:
json_dict = {}
# 获取商品的每个字段,键值对形式
json_dict['name'] = good.name
json_dict['category'] = good.category.name
json_dict['market_price'] = good.market_price
json_list.append(json_dict)

from django.http import HttpResponse
import json

# 返回json,一定要指定类型content_type='application/json'
return HttpResponse(json.dumps(json_list), content_type='application/json')

配置url

1
path('goods/', GoodsListView.as_view(), name='goods'),

通过浏览器,可以获取商品列表信息的json数据

好像还可以,这里继续添加数据

1
json_dict["add_time"] = good.add_time

浏览器访问

我们会发现报错了,这种方法是行不通的

django的serializer序列化model

model_to_dict

当字段比较多时,一个字段一个字段的提取很麻烦,可以用model_to_dict,将model整个转化为dict

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class GoodsListView(View):
def get(self, request):
# 通过django的view实现商品列表页
json_list = []
# 获取所有商品
goods = Goods.objects.all()
# for good in goods:
# json_dict = {}
# #获取商品的每个字段,键值对形式
# json_dict['name'] = good.name
# json_dict['category'] = good.category.name
# json_dict['market_price'] = good.market_price
# json_list.append(json_dict)

from django.forms.models import model_to_dict
for good in goods:
json_dict = model_to_dict(good)
json_list.append(json_dict)

from django.http import HttpResponse
import json
# 返回json,一定要指定类型content_type='application/json'
return HttpResponse(json.dumps(json_list), content_type='application/json')

打开浏览器访问

发现依然报错,ImageFieldFile 和add_time字段不能序列化
这种方法依然有局限性

django serializer

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class GoodsListView(View):
def get(self, request):
# 通过django的view实现商品列表页
json_list = []
# 获取所有商品
goods = Goods.objects.all()
# for good in goods:
# json_dict = {}
# #获取商品的每个字段,键值对形式
# json_dict['name'] = good.name
# json_dict['category'] = good.category.name
# json_dict['market_price'] = good.market_price
# json_list.append(json_dict)

import json
from django.core import serializers
from django.http import JsonResponse

json_data = serializers.serialize('json', goods)
json_data = json.loads(json_data)
return JsonResponse(json_data, safe=False)


看着效果挺不错的,数据都加载进来了,但是缺点也挺明显的

  1. 字段是写死的,不灵活
  2. image字段不完整

这些缺点drf都可以帮我们来完成

drf实现列表页

安装插件

1
2
pip install coreapi                         drf的文档支持
pip install django-guardian drf对象级别的权限支持

APIview方式实现商品列表页

配置urls

1
2
path('api-auth/',include('rest_framework.urls')),
path('docs/',include_docs_urls(title='生鲜超市')),

配置rest_framework

1
2
3
INSTALLED_APPS = [
'rest_framework',
]

goods文件夹下面新建serializers.py
这里先写三个字段

1
2
3
4
5
6
7
from rest_framework import serializers


class GoodsSerializer(serializers.Serializer):
name = serializers.CharField(required=True, max_length=100)
click_num = serializers.IntegerField(default=0)
goods_front_image = serializers.ImageField()

goods/views.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from rest_framework.response import Response
from rest_framework.views import APIView

from goods.models import Goods
from goods.serializers import GoodsSerializer


class GoodsListView(APIView):
'''
商品列表
'''

def get(self, request, format=None):
goods = Goods.objects.all()
goods_serialzer = GoodsSerializer(goods, many=True)
return Response(goods_serialzer.data)

修改urls的GoodsListView的引入
浏览器访问

这是drf渲染的界面
可以看到image字段已经帮我们补全了

drf的Modelserializer实现商品列表页

上面是用Serializer实现的,需要自己手动添加字段,如果用Modelserializer,会更加的方便,直接用__all__就可以全部序列化
serializers.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from rest_framework import serializers

from goods.models import Goods


# class GoodsSerializer(serializers.Serializer):
# name = serializers.CharField(required=True, max_length=100)
# click_num = serializers.IntegerField(default=0)
# goods_front_image = serializers.ImageField()

# ModelSerializer实现商品列表页
class GoodsSerializer(serializers.ModelSerializer):
class Meta:
model = Goods
fields = '__all__'


外键被序列化为id,如果想要显示外键字段的信息,可以使用Serialzer的嵌套功能
serializers.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class CategorySerializer(serializers.ModelSerializer):
class Meta:
model = GoodsCategory
fields = "__all__"


# ModelSerializer实现商品列表页
class GoodsSerializer(serializers.ModelSerializer):
# 覆盖外键字段
category = CategorySerializer()

class Meta:
model = Goods
fields = '__all__'


乐意看到,category字段显示的已经是详细信息了,不再是一个id了

GenericView实现商品列表页

mixins和generic一起使用
GenericAPIView继承APIView,封装了很多方法,比APIView功能更强大
用的时候需要定义queryset和serializer_class
GenericAPIView里面默认为空
ListModelMixin里面list方法帮我们做好了分页和序列化的工作,只要调用就好了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from rest_framework import mixins, generics
from rest_framework.response import Response
from rest_framework.views import APIView

from goods.models import Goods
from goods.serializers import GoodsSerializer


class GoodsListView(mixins.ListModelMixin, generics.GenericAPIView):
'''
商品列表页
'''
queryset = Goods.objects.all()
serializer_class = GoodsSerializer

def get(self, request, *args, **kwargs):
return self.list(request, *args, **kwargs)

如果不写get方法的话,是没法通过get请求访问的
这样看起来代码比之前的简洁一点了
我们还可以通过给继承ListAPIView来让代码更加简介
ListAPIView源代码如下

1
2
3
4
5
6
7
class ListAPIView(mixins.ListModelMixin,
GenericAPIView):
"""
Concrete view for listing a queryset.
"""
def get(self, request, *args, **kwargs):
return self.list(request, *args, **kwargs)

可以看到ListAPIView继承了mixins.ListModelMixingenerics.GenericAPIView
而且帮我们实现了get方法,和我们自己写的get方法一样
这样的话,我们的代码就长这样了

1
2
3
4
5
6
class GoodsListView(generics.ListAPIView):
'''
商品列表页
'''
queryset = Goods.objects.all()
serializer_class = GoodsSerializer

运行结果和之前的一样,但是代码只有两行

添加分页功能

官网示例:
http://www.django-rest-framework.org/api-guide/pagination/#setting-the-pagination-style

settings.py

1
2
3
4
REST_FRAMEWORK = {
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
'PAGE_SIZE': 1,
}

DEFAULT_PAGINATION_CLASS: 分页所使用的类
PAGE_SIZE: 每页显示的数量
下面的图片路径也已经进行了补全,连域名都加上了

运行访问时可能会有一个警告
UnorderedObjectListWarning: Pagination may yield inconsistent results with an unordered object_list: <class 'goods.models.Goods'> QuerySet.
是因为我们没有对取出的数据进行排序

1
queryset = Goods.objects.all().order_by('id')

自定义分页功能

http://www.django-rest-framework.org/api-guide/pagination/#modifying-the-pagination-style
首先注释掉settings.py中的分页
goods/views.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class GoodsPagination(PageNumberPagination):
'''
商品列表自定义分页
'''
# 默认每页显示的个数
page_size = 10
# 可以动态改变每页显示的个数
page_size_query_param = 'page_size'
# 页码参数 http://127.0.0.1:8000/goods/?page=2&page_size=30
page_query_param = 'page'
# 每页最多能显示多少体条
# 仅当 page_size_query_param 设置时有效
max_page_size = 20


class GoodsListView(generics.ListAPIView):
'''
商品列表页
'''
queryset = Goods.objects.all()
serializer_class = GoodsSerializer
pagination_class = GoodsPagination

page_size_query_param: 默认每页显示的是10条数据,可以通过这个变量来改变每页显示的数量
http://127.0.0.1:8000/goods/?page=2&page_size=30
这个数量又受到max_page_size这个变量的控制
当我们想要每页显示30条数据的时候,明显的>20,所以每页只显示20条数据

viewsets和router完成商品列表页

主要用到viewsets中的GenericViewSet

1
2
3
4
5
6
7
class GenericViewSet(ViewSetMixin, generics.GenericAPIView):
"""
The GenericViewSet class does not provide any actions by default,
but does include the base set of generic view behavior, such as
the `get_object` and `get_queryset` methods.
"""
pass

ViewSetMixin中重写了as_view方法,可以将action和函数进行绑定

1
2
3
4
5
6
7
class GoodsListViewSet(mixins.ListModelMixin, viewsets.GenericViewSet):
'''
商品列表页
'''
queryset = Goods.objects.all()
serializer_class = GoodsSerializer
pagination_class = GoodsPagination

urls.py

1
2
3
4
5
6
from goods.views import GoodsListViewSet
goods_list = GoodsListViewSet.as_view({
'get': 'list',
})

path('goods/', goods_list, name='goods'),

通过viewset的as_view方法,将get请求和list方法进行绑定
但是这样的话需要手动绑定比较麻烦,drf提供了一种更简单的使用方法
http://www.django-rest-framework.org/tutorial/6-viewsets-and-routers/#using-routers

1
2
3
4
5
6
7
from rest_framework.routers import DefaultRouter

router = DefaultRouter()
router.register(r'goods', GoodsListViewSet, base_name='goods')


re_path('^', include(router.urls)),

drf的APIView、GenericView、viewsets和router的简单分析

这是GoodsListViewSet的继承关系

GenericViewSet 是最高的一层

往下

GenericViewSet(viewsets) —-drf

  GenericAPIView —drf

    APIView —drf

      View     —-django

这些view功能的不同,主要的是有mixin的存在
mixins总共有五种:
  CreateModelMixin
  ListModelMixin
  UpdateModelMixin
  RetrieveModelMixin
  DestoryModelMixin

Router提供了自动绑定的功能

drf的request和response介绍

http://www.django-rest-framework.org/api-guide/requests/

http://www.django-rest-framework.org/api-guide/responses/

drf的过滤

在使用drf的过滤器之前,请先安装django-filter

1
pip install django-filter

http://www.django-rest-framework.org/api-guide/filtering/#api-guide

django-filter官网

添加到INSTALLED_APPS里面

1
2
3
INSTALLED_APPS = [
'django_filters',
]

在goods目录下新建filters.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import django_filters

from goods.models import Goods


class GoodsFilter(django_filters.rest_framework.FilterSet):
'''
商品过滤的类
'''
# 两个参数,field_name是要过滤的字段,lookup是执行的行为,‘小与等于本店价格’
price_min = django_filters.NumberFilter(field_name="shop_price", lookup_expr='gte')
price_max = django_filters.NumberFilter(field_name="shop_price", lookup_expr='lte')

class Meta:
model = Goods
fields = ['price_min', 'price_max']

goods/views.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from django_filters.rest_framework import DjangoFilterBackend
from goods.filters import GoodsFilter


class GoodsListViewSet(mixins.ListModelMixin, viewsets.GenericViewSet):
'''
商品列表页
'''
queryset = Goods.objects.all().order_by('id')
serializer_class = GoodsSerializer
pagination_class = GoodsPagination
filter_backends = (DjangoFilterBackend,)
# 自定义过滤器
filter_class = GoodsFilter

drf的搜索和排序

http://www.django-rest-framework.org/api-guide/filtering/#searchfilter

http://www.django-rest-framework.org/api-guide/filtering/#orderingfilter

这里的排序,搜索使用的都是rest_framework里面的包,而不是django_filters里面的包

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from rest_framework.filters import SearchFilter, OrderingFilter

class GoodsListViewSet(mixins.ListModelMixin, viewsets.GenericViewSet):
'''
商品列表页, 分页, 过滤, 排序
'''
queryset = Goods.objects.all().order_by('id')
serializer_class = GoodsSerializer
pagination_class = GoodsPagination
filter_backends = (DjangoFilterBackend, SearchFilter, OrderingFilter)
# 自定义过滤器
filter_class = GoodsFilter
# 搜索,默认模糊查询
search_fields = ('name', 'goods_brief')
# 排序
ordering_fields = ('shop_price', 'add_time')


短短几行代码,就完成了商品列表页的分页,过滤,排序功能页的分页,过滤,排序功能

首页商品类别数据显示

商品分类接口

大概需要两个,一个显示三个类别


一个显示类别及类别下的全部商品

现在开始写商品的接口
首先编写三个分类的serializer

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
class CategorySerializer3(serializers.ModelSerializer):
'''
三级分类
'''

class Meta:
model = GoodsCategory
fields = "__all__"


class CategorySerializer2(serializers.ModelSerializer):
'''
二级分类
'''
# 在parent_category字段中定义的related_name="sub_cat"
sub_cat = CategorySerializer3(many=True)

class Meta:
model = GoodsCategory
fields = "__all__"


class CategorySerializer(serializers.ModelSerializer):
"""
商品一级类别序列化
"""
sub_cat = CategorySerializer2(many=True)

class Meta:
model = GoodsCategory
fields = "__all__"

然后编写视图函数

1
2
3
4
5
6
7
8
class CategoryViewSet(mixins.ListModelMixin, mixins.RetrieveModelMixin, viewsets.GenericViewSet):
'''
list:
商品分类列表数据
'''

queryset = GoodsCategory.objects.filter(category_type=1)
serializer_class = CategorySerializer

配置url

1
2
# 配置Category的url
router.register(r'categorys', CategoryViewSet, base_name="categorys")

注释的内容会显示在文档中
mixins.RetrieveModelMixin可以查看单个分类
前面已经配置过文档的url了

vue展示商品分类数据

接口相关代码都放在src/api/api.js里面,调试接口的时候我们首先需要新建一个自己的host,然后替换要调试的host

1
let local_host = 'http://127.0.0.1:8000'

替换商品类别默认的host

1
2
3
4
5
6
7
8
9
//获取商品类别信息
export const getCategory = params => {
if('id' in params){
return axios.get(`${local_host}/categorys/`+params.id+'/');
}
else {
return axios.get(`${local_host}/categorys/`, params);
}
};

打开浏览器,可以看到,数据并没有显示出来,
是因为这涉及到了跨域问题,接下来就解决跨域的问题

后端服务器解决跨域问题的方法

https://github.com/ottoyiu/django-cors-headers
安装模块

1
pip install django-cors-headers

添加到INSTALL_APPS中

1
2
3
INSTALLED_APPS = (
'corsheaders',
)

添加中间件
CorsMiddleware应尽可能放置,特别是在可以生成响应的任何中间件之前,例如Django CommonMiddleware或Whitenoise WhiteNoiseMiddleware.如果不是之前,它将无法将CORS标头添加到这些响应中.

此外,如果你使用CORS_REPLACE_HTTPS_REFERER它应该放在Django之前CsrfViewMiddleware(见下文)
这里放在最前面

1
2
3
4
5
6
7
8
9
10
11
MIDDLEWARE = [
# 跨域
'corsheaders.middleware.CorsMiddleware',
'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设置中配置了中间件.您必须添加允许执行跨站点请求的主机 CORS_ORIGIN_WHITELIST,或者设置CORS_ORIGIN_ALLOW_ALL以True 允许所有主机.

这里先允许所有主机跨域,等部署后可以直接将主机ip或域名添加到CORS_ORIGIN_WHITELIST

1
2
# 跨域
CORS_ORIGIN_ALLOW_ALL = True

打开浏览器进行访问,会发现导航里面没有数据,这是因为后台没有进行设置,需要在后台进行设置

vue展示商品列表页数据

商品列表页会判断我们是serach还是getGoods

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
getListData() {
if(this.pageType=='search'){
getGoods({
search: this.searchWord, //搜索关键词
}).then((response)=> {
this.listData = response.data.results;
this.proNum = response.data.count;
}).catch(function (error) {
console.log(error);
});
}else {
getGoods({
page: this.curPage, //当前页码
top_category: this.top_category, //商品类型
ordering: this.ordering, //排序类型
pricemin: this.pricemin, //价格最低 默认为‘’ 即为不选价格区间
pricemax: this.pricemax // 价格最高 默认为‘’
}).then((response)=> {

this.listData = response.data.results;
this.proNum = response.data.count;
}).catch(function (error) {
console.log(error);
});
}

},

根据前端内容修改后端分页的参数

1
2
3
4
5
6
7
8
9
10
11
12
13
class GoodsPagination(PageNumberPagination):
'''
商品列表自定义分页
'''
# 默认每页显示的个数
page_size = 12
# 可以动态改变每页显示的个数
page_size_query_param = 'page_size'
# 页码参数 http://127.0.0.1:8000/goods/?page=2&page_size=30
page_query_param = 'page'
# 每页最多能显示多少体条
# 仅当 page_size_query_param 设置时有效
max_page_size = 20

通过vue代码可以看到url参数中有一个top_category
需要给这个参数添加过滤方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class GoodsFilter(django_filters.rest_framework.FilterSet):
'''
商品过滤的类
'''
# 两个参数,field_name是要过滤的字段,lookup是执行的行为,‘小与等于本店价格’
pricemin = django_filters.NumberFilter(field_name="shop_price", lookup_expr='gte', label='最低价')
pricemax = django_filters.NumberFilter(field_name="shop_price", lookup_expr='lte', label='最高价')
top_category = django_filters.NumberFilter(method='top_category_filter', label='分类ID')

# 自定义过滤方法,不管当前点击的是一级分类二级分类还是三级分类,都能找到.
def top_category_filter(self, queryset, name, value):
return queryset.filter(Q(category_id=value) | Q(category__parent_category_id=value) | Q(
category__parent_category__parent_category_id=value))

class Meta:
model = Goods
fields = ['pricemin', 'pricemax']

通过浏览器可以进行测试
在后台查找某一分类下的商品,然后在前台点击进行对比

修改排序
前台是按照销量和价格进行排序的,修改后端代码

1
2
   #排序
ordering_fields = ('sold_num', 'shop_price')

分类过滤
价格区间过滤
显示商品数量
分页
搜索

注册和登陆

drf的认证

http://www.django-rest-framework.org/api-guide/authentication/

settings.py文件的配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
INSTALLED_APPS = (
...
'rest_framework.authtoken'
)

REST_FRAMEWORK = {
# 'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
# 'PAGE_SIZE': 10,
'DEFAULT_AUTHENTICATION_CLASSES': (
'rest_framework.authentication.BasicAuthentication',
'rest_framework.authentication.SessionAuthentication',
'rest_framework.authentication.TokenAuthentication',
)
}

运行migrations和migrate,会生成一张authtoken_token表,里面是没有数据的

url配置

1
2
3
from rest_framework.authtoken import views

path('api-token-auth/', views.obtain_auth_token)

使用postman进行测试

此时authtoken_token表已经生成了一条数据

使用刚才的token再进行测试

token的写法是固定的
http://www.django-rest-framework.org/api-guide/authentication/#tokenauthentication
断点调试,查看request.user

如果输入了错误的token,将会返回401

实际上这些页面是公开查看的,不应该配置全局的token,
可以在某些视图函数中进行认证

1
authentication_classes = (TokenAuthentication,)

drf的token缺点

  • 保存在数据库中,如果是一个分布式的系统,就非常麻烦
  • token永久有效,没有过期时间.

jwt完成用户认证

关于jwt的详情请自行查询

https://github.com/GetBlimp/django-rest-framework-jwt/

安装

1
pip install djangorestframework-jwt

settings.py

1
2
3
4
5
6
7
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': (
'rest_framework_jwt.authentication.JSONWebTokenAuthentication',
'rest_framework.authentication.SessionAuthentication',
'rest_framework.authentication.BasicAuthentication',
),
}

urls.py

1
path('jwt-token-auth/', obtain_jwt_token),

使用postman对这个接口进行测试

vue和jwt接口调试

前端登陆的接口是login

1
2
3
4
//登录
export const login = params => {
return axios.post(`${local_host}/login/`, params)
}

修改一下后台的代码

1
2
# jwt token
path('login/', obtain_jwt_token),

jwt接口默认采用的是用户名和密码登录验证,如果用手机登录的话,就会验证失败,需要自定义一个用户验证

settings中配置

1
2
3
4
AUTHENTICATION_BACKENDS = (
'users.views.CustomBackend',

)

users/views.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
from django.contrib.auth import get_user_model
from django.contrib.auth.backends import ModelBackend
from django.db.models import Q
from django.shortcuts import render

# Create your views here.
from users.models import UserProfile

User = get_user_model()


class CustomBackend(ModelBackend):
"""
自定义用户验证
"""

def authenticate(self, username=None, password=None, **kwargs):
try:
# 用户名和手机都能登录
user = User.objects.get(
Q(username=username) | Q(mobile=username))
if user.check_password(password):
return user
except Exception as e:
return None

JWT过期时间设置
settings.py

1
2
3
4
5
# 有效期限
JWT_AUTH = {
'JWT_EXPIRATION_DELTA': datetime.timedelta(days=7), # 也可以设置seconds=300
'JWT_AUTH_HEADER_PREFIX': 'JWT', # JWT跟前端保持一致,比如“token”这里设置成JWT
}

短信验证码

这里使用云片网来发送短信验证码
apps下新建utils包,再新建yunpian.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import json

import requests


class YunPian(object):

def __init__(self, api_key):
self.api_key = api_key
self.single_send_url = "https://sms.yunpian.com/v2/sms/single_send.json"

def send_sms(self, code, mobile):
parmas = {
"apikey": self.api_key,
"mobile": mobile,
"text": "【南工在线超市】您的验证码是{code}.如非本人操作,请忽略本短信".format(code=code)
}

response = requests.post(self.single_send_url, data=parmas)
re_dict = json.loads(response.text)
return re_dict


if __name__ == "__main__":
#
yun_pian = YunPian("70c24xxxxxxxx70a49")
yun_pian.send_sms("2018", "1xxxxxxxx0")

参数参考开发文档
https://www.yunpian.com/doc/zh_CN/scene/smsverify.html

drf实现发送短信验证码接口

手机号验证手机号是否合法, 是否已经注册
settings.py

1
2
# 手机号码正则表达式
REGEX_MOBILE = "^1[358]\d{9}$|^147\d{8}$|^176\d{8}$"

users下新建serializers.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
import re
from datetime import datetime, timedelta

from django.contrib.auth import get_user_model
from rest_framework import serializers

from MxShop.settings import REGEX_MOBILE
from users.models import VerifyCode

User = get_user_model()


class SmsSerializer(serializers.Serializer):
mobile = serializers.CharField(max_length=11)

# 函数名必须:validate + 验证字段名
def validate_mobile(self, mobile):
"""
手机号码验证
"""
# 是否已经注册
if User.objects.filter(mobile=mobile).count():
raise serializers.ValidationError("用户已经存在")

# 是否合法
if not re.match(REGEX_MOBILE, mobile):
raise serializers.ValidationError("手机号码非法")

# 验证码发送频率
# 60s内只能发送一次
one_mintes_ago = datetime.now() - timedelta(hours=0, minutes=1, seconds=0)
if VerifyCode.objects.filter(add_time__gt=one_mintes_ago, mobile=mobile).count():
raise serializers.ValidationError("请求过于频繁,请稍后重试!")

return mobile

APIKEY加到settings里面

1
2
# 云片网APIKEY
APIKEY = "xxxxx327d4be01608xxxxxxxxxx"

views后台逻辑
我们要重写CreateModelMixin的create方法,源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class CreateModelMixin(object):
"""
Create a model instance.
"""
def create(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
self.perform_create(serializer)
headers = self.get_success_headers(serializer.data)
return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)

def perform_create(self, serializer):
serializer.save()

def get_success_headers(self, data):
try:
return {'Location': str(data[api_settings.URL_FIELD_NAME])}
except (TypeError, KeyError):
return {}

这个函数的作用就是创建一个model实例
users/views.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
class SmsCodeViewset(CreateModelMixin, viewsets.GenericViewSet):
"""
发送短信验证码
"""
serializer_class = SmsSerializer

def generate_code(self):
"""
生成四位数字的验证码
:return:
"""
seeds = "1234567890"
random_str = []
for i in range(4):
random_str.append(choice(seeds))

return "".join(random_str)

def create(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)

mobile = serializer.validated_data["mobile"]

yun_pian = YunPian(APIKEY)

code = self.generate_code()

sms_status = yun_pian.send_sms(code=code, mobile=mobile)

if sms_status["code"] != 0:
return Response({
"mobile": sms_status["msg"]
}, status=status.HTTP_400_BAD_REQUEST)
else:
code_record = VerifyCode(code=code, mobile=mobile)
code_record.save()
return Response({
"mobile": mobile
}, status=status.HTTP_201_CREATED)

urls.py

1
2
# 配置code的url
router.register(r'code', SmsCodeViewset, base_name="code")

打开浏览器进行测试,输入正确的手机号码即可接收短信,
错误的手机号码或者已存在的会报相应的错误信息

注册

注册时需要填入手机号,验证码和密码
修改UserProfile中mobile字段

1
mobile = models.CharField(verbose_name="电话",max_length=11, blank=True, null=True)

设置允许为空,因为前端只有一个值,是username,所以mobile可以为空
users/serializers.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
class UserRegSerializer(serializers.ModelSerializer):
'''
用户注册
'''
#UserProfile中没有code字段,这里需要自定义一个code序列化字段
code = serializers.CharField(required=True, write_only=True, max_length=4, min_length=4,
error_messages={
"blank": "请输入验证码",
"required": "请输入验证码",
"max_length": "验证码格式错误",
"min_length": "验证码格式错误"
},
help_text="验证码")
#验证用户名是否存在
username = serializers.CharField(label="用户名", help_text="用户名", required=True, allow_blank=False,
validators=[UniqueValidator(queryset=User.objects.all(), message="用户已经存在")])

#验证code
def validate_code(self, code):
# 用户注册,已post方式提交注册信息,post的数据都保存在initial_data里面
#username就是用户注册的手机号,验证码按添加时间倒序排序,为了后面验证过期,错误等
verify_records = VerifyCode.objects.filter(mobile=self.initial_data["username"]).order_by("-add_time")

if verify_records:
# 最近的一个验证码
last_record = verify_records[0]
# 有效期为五分钟.
five_mintes_ago = datetime.now() - timedelta(hours=0, minutes=5, seconds=0)
if five_mintes_ago > last_record.add_time:
raise serializers.ValidationError("验证码过期")

if last_record.code != code:
raise serializers.ValidationError("验证码错误")

else:
raise serializers.ValidationError("验证码错误")

# 所有字段.attrs是字段验证合法之后返回的总的dict
def validate(self, attrs):
#前端没有传mobile值到后端,这里添加进来
attrs["mobile"] = attrs["username"]
#code是自己添加得,数据库中并没有这个字段,验证完就删除掉
del attrs["code"]
return attrs

class Meta:
model = User
fields = ('username','code','mobile')

write_only等字段的含义
http://www.django-rest-framework.org/api-guide/fields/#core-arguments

users/views.py

1
2
3
4
5
class UserViewset(CreateModelMixin,viewsets.GenericViewSet):
'''
用户
'''
serializer_class = UserRegSerializer

配置url

1
router.register(r'users', UserViewset, base_name="users")

django信号量实现用户密码修改

user/views.py

1
2
3
4
5
6
class UserViewset(CreateModelMixin,viewsets.GenericViewSet):
'''
用户
'''
serializer_class = UserRegSerializer
queryset = User.objects.all()

user/serializer.py添加

1
fields = ('username','code','mobile','password')

password不能明文显示和加密保存

需要重载Create方法

1
2
3
4
5
6
7
8
9
10
11
# 输入密码的时候不显示明文
password = serializers.CharField(
style={'input_type': 'password'}, label="密码", write_only=True
)

# 密码加密保存
def create(self, validated_data):
user = super(UserRegSerializer, self).create(validated_data=validated_data)
user.set_password(validated_data["password"])
user.save()
return user

使用信号量替代重载create方法
users下面创建signals.py

1
2
3
4
5
6
7
8
9
10
11
12
13
from django.contrib.auth import get_user_model
from django.db.models.signals import post_save
from django.dispatch import receiver

User = get_user_model()


@receiver(post_save, sender=User)
def create_user(sender, instance=None, created=False, **kwargs):
if created:
password = instance.password
instance.set_password(password)
instance.save()

users/apps.py

1
2
3
4
5
6
7
8
9
from django.apps import AppConfig


class UsersConfig(AppConfig):
name = 'users'
verbose_name = "用户管理"

def ready(self):
import users.signals

现在添加用户的时候,密码就会自动加密存储了

信号量的相关信息
https://docs.djangoproject.com/zh-hans/2.0/topics/signals/

注册的serializer

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
class UserRegSerializer(serializers.ModelSerializer):
'''
用户注册
'''
# UserProfile中没有code字段,这里需要自定义一个code序列化字段
code = serializers.CharField(required=True, write_only=True, max_length=4, min_length=4,
error_messages={
"blank": "请输入验证码",
"required": "请输入验证码",
"max_length": "验证码格式错误",
"min_length": "验证码格式错误"
},
help_text="验证码")
# 验证用户名是否存在
username = serializers.CharField(label="用户名", help_text="用户名", required=True, allow_blank=False,
validators=[UniqueValidator(queryset=User.objects.all(), message="用户已经存在")])

# 输入密码的时候不显示明文
password = serializers.CharField(
style={'input_type': 'password'}, label="密码", write_only=True
)

# # 密码加密保存
# def create(self, validated_data):
# user = super(UserRegSerializer, self).create(validated_data=validated_data)
# user.set_password(validated_data["password"])
# user.save()
# return user


# 验证code
def validate_code(self, code):
# 用户注册,已post方式提交注册信息,post的数据都保存在initial_data里面
# username就是用户注册的手机号,验证码按添加时间倒序排序,为了后面验证过期,错误等
verify_records = VerifyCode.objects.filter(mobile=self.initial_data["username"]).order_by("-add_time")

if verify_records:
# 最近的一个验证码
last_record = verify_records[0]
# 有效期为五分钟.
five_mintes_ago = datetime.now() - timedelta(hours=0, minutes=5, seconds=0)
if five_mintes_ago > last_record.add_time:
raise serializers.ValidationError("验证码过期")

if last_record.code != code:
raise serializers.ValidationError("验证码错误")

else:
raise serializers.ValidationError("验证码错误")

# 所有字段.attrs是字段验证合法之后返回的总的dict

def validate(self, attrs):
# 前端没有传mobile值到后端,这里添加进来
attrs["mobile"] = attrs["username"]
# code是自己添加得,数据库中并没有这个字段,验证完就删除掉
del attrs["code"]
return attrs

class Meta:
model = User
fields = ('username', 'code', 'mobile', 'password')

vue和注册功能联调

生成token的两个重要步骤,一是payload,二是encode

users/views.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class UserViewset(CreateModelMixin,viewsets.GenericViewSet):
'''
用户注册
'''
serializer_class = UserRegSerializer
queryset = User.objects.all()

def create(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
user = self.perform_create(serializer)

# 注册成功直接生成token,自动登陆
re_dict = serializer.data
payload = jwt_payload_handler(user)
re_dict["token"] = jwt_encode_handler(payload)
re_dict["name"] = user.name if user.name else user.username

headers = self.get_success_headers(serializer.data)

# 返回的不是serializer.data,而是我们自己写的re_dict
return Response(re_dict, status=status.HTTP_201_CREATED, headers=headers)

def perform_create(self, serializer):
return serializer.save()

登陆和注册到这里就结束了
退出时只需要在前端代码中清除token和name就行了
jwt无状态,后端不保存token

商品详情页功能

商品详情页和CategoryViewSet类似,只需要多继承一个类(mixins.RetrieveModelMixin)就可以了

1
class GoodsListViewSet(mixins.ListModelMixin,mixins.RetrieveModelMixin, viewsets.GenericViewSet):

商品轮播图是一个外键,序列化外键用嵌套的方法来实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 轮播图
class GoodsImageSerializer(serializers.ModelSerializer):
class Meta:
model = GoodsImage
fields = ("image",)


# ModelSerializer实现商品列表页
class GoodsSerializer(serializers.ModelSerializer):
# 覆盖外键字段
category = CategorySerializer()
# images是数据库中设置的related_name="images",把轮播图嵌套进来
images = GoodsImageSerializer(many=True)

class Meta:
model = Goods
fields = '__all__'

打开浏览器,找到一个商品打开,查看详情
数据应该已经填充进去了,如果商品详情中的图片未显示,
打开根目录下的proxy.js文件,将里面的url修改为本地即可

1
2
3
module.exports = {
"/": "http://127.0.0.1:8000"
};

热卖商品接口实现

只需要在过滤器中增加is_hot就可以了

goods/filters.py里的GoodsFilter添加is_hot

1
2
3
class Meta:
model = Goods
fields = ['pricemin', 'pricemax','is_hot']

在后台设置商品的is_hot为True,然后前端就可以显示出来了

用户收藏接口实现

user_operation/serializers.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
from rest_framework import serializers
from rest_framework.validators import UniqueTogetherValidator

from user_operation.models import UserFav


class UserFavSerializer(serializers.ModelSerializer):
# 获取当前登录的用户
user = serializers.HiddenField(
default=serializers.CurrentUserDefault()
)

class Meta:
# validate实现唯一联合,一个商品只能收藏一次
validators = [
UniqueTogetherValidator(
queryset=UserFav.objects.all(),
fields=('user', 'goods'),
# message的信息可以自定义
message="该商品已经收藏"
)
]
model = UserFav
# 收藏的时候需要返回商品的id,因为取消收藏的时候必须知道商品的id是多少
fields = ("user", "goods", 'id')

user_operation/views.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from rest_framework import viewsets, mixins

from user_operation.models import UserFav
from user_operation.serializers import UserFavSerializer


class UserFavViewset(viewsets.GenericViewSet, mixins.ListModelMixin, mixins.CreateModelMixin, mixins.DestroyModelMixin):
'''
list:
获取用户的所有收藏
create:
添加收藏
destroy:
取消收藏
'''
queryset = UserFav.objects.all()
serializer_class = UserFavSerializer

配置url

1
2
# 配置用户收藏的url
router.register(r'userfavs', UserFavViewset, base_name="userfavs")

进行测试
当重复收藏某个商品时,会报错

drf的权限认证

http://www.django-rest-framework.org/api-guide/permissions/
utils文件夹下新建permissions.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from rest_framework import permissions


class IsOwnerOrReadOnly(permissions.BasePermission):
"""
Object-level permission to only allow owners of an object to edit it.
Assumes the model instance has an `owner` attribute.
"""

def has_object_permission(self, request, view, obj):
# Read permissions are allowed to any request,
# so we'll always allow GET, HEAD or OPTIONS requests.
if request.method in permissions.SAFE_METHODS:
return True

# Instance must have an attribute named `owner`.
return obj.user == request.user

user_operation/views.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class UserFavViewset(viewsets.GenericViewSet, mixins.ListModelMixin, mixins.CreateModelMixin, mixins.DestroyModelMixin):
'''
list:
获取用户的所有收藏
create:
添加收藏
destroy:
取消收藏
'''
serializer_class = UserFavSerializer
# permission是用来做权限判断的
# IsAuthenticated:必须登录用户;IsOwnerOrReadOnly:必须是当前登录的用户
permission_classes = (IsAuthenticated, IsOwnerOrReadOnly)
# auth使用来做用户认证的
authentication_classes = (JSONWebTokenAuthentication, SessionAuthentication)
# 搜索的字段
lookup_field = 'goods_id'

def get_queryset(self):
# 只能查看当前登录用户的收藏,不会获取所有用户的收藏
return UserFav.objects.filter(user=self.request.user)

只有登录用户才可以收藏
用户只能获取自己的收藏,不能获取所有用户的收藏
JSONWebTokenAuthentication认证不需要全局配置
删除settings中的’rest_framework_jwt.authentication.JSONWebTokenAuthentication’,

将前端代码中的地址替换为本地,进行测试

搜索字段是在获取当前用户后操作的,
也就是说搜索的是当前用户的收藏,
而不是在所有用户中进行搜索,

1
2
3
4
5
6
7
8
9
10
11
12
def get_object(self):
"""
Returns the object the view is displaying.

You may want to override this if you need to provide non-standard
queryset lookups. Eg if objects are referenced using multiple
keyword arguments in the url conf.
"""
queryset = self.filter_queryset(self.get_queryset())

# Perform the lookup filtering.
lookup_url_kwarg = self.lookup_url_kwarg or self.lookup_field

收藏功能结束

个人中心功能开发

drf文档注释
http://www.django-rest-framework.org/topics/documenting-your-api/

动态设置serializer和permission获取用户信息

获取详情只需要添加一个mixins.RetrieveModelMixin,就行了

用户详情的序列化
users/serializers.py

1
2
3
4
5
6
7
class UserDetailSerializer(serializers.ModelSerializer):
"""
用户详情
"""
class Meta:
model = User
fields = ("name", "gender", "birthday", "email","mobile")

views.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
class UserViewset(mixins.CreateModelMixin, mixins.RetrieveModelMixin, mixins.UpdateModelMixin, viewsets.GenericViewSet):
'''
用户信息管理
create:
用户注册
retrieve:
个人信息
update:
修改个人信息
'''
queryset = User.objects.all()
authentication_classes = (JSONWebTokenAuthentication, authentication.SessionAuthentication)

def create(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
user = self.perform_create(serializer)

# 注册成功直接生成token,自动登陆
re_dict = serializer.data
payload = jwt_payload_handler(user)
re_dict["token"] = jwt_encode_handler(payload)
re_dict["name"] = user.name if user.name else user.username

headers = self.get_success_headers(serializer.data)

# 返回的不是serializer.data,而是我们自己写的re_dict
return Response(re_dict, status=status.HTTP_201_CREATED, headers=headers)

# 这里需要动态权限配置
# 1.用户注册的时候不应该有权限限制
# 2.当想获取用户详情信息的时候,必须登录才行
def get_permissions(self):
if self.action == "retrieve":
return [permissions.IsAuthenticated(), ]
elif self.action == "create":
return []

return []

# 这里需要动态选择用哪个序列化方式
# 1.UserRegSerializer(用户注册),只返回username和mobile,会员中心页面需要显示更多字段,所以要创建一个UserDetailSerializer
# 2.问题又来了,如果注册的使用userdetailSerializer,又会导致验证失败,所以需要动态的使用serializer
def get_serializer_class(self):
if self.action == "retrieve":
return UserDetailSerializer
elif self.action == "create":
return UserRegSerializer

return UserDetailSerializer

# 虽然继承了Retrieve可以获取用户详情,但是并不知道用户的id,所有要重写get_object方法
# 重写get_object方法,就知道是哪个用户了
def get_object(self):
return self.request.user

def perform_create(self, serializer):
return serializer.save()

用户个人信息修改,只需要继承mixins.UpdateModelMixin就可以了

用户收藏

user_operation/serializer.py

1
2
3
4
5
6
7
8
9
10
11
class UserFavDetailSerializer(serializers.ModelSerializer):
'''
用户收藏详情
'''

# 通过商品id获取收藏的商品,需要嵌套商品的序列化
goods = GoodsSerializer()

class Meta:
model = UserFav
fields = ("goods", "id")

views.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class UserFavViewset(viewsets.GenericViewSet, mixins.ListModelMixin, mixins.CreateModelMixin, mixins.DestroyModelMixin):
'''
list:
获取用户的所有收藏
create:
添加收藏
destroy:
取消收藏
'''
# permission是用来做权限判断的
# IsAuthenticated:必须登录用户;IsOwnerOrReadOnly:必须是当前登录的用户
permission_classes = (IsAuthenticated, IsOwnerOrReadOnly)
# auth使用来做用户认证的
authentication_classes = (JSONWebTokenAuthentication, SessionAuthentication)
# 搜索的字段
lookup_field = 'goods_id'

def get_queryset(self):
# 只能查看当前登录用户的收藏,不会获取所有用户的收藏
return UserFav.objects.filter(user=self.request.user)

# 动态选择serializer
def get_serializer_class(self):
if self.action == "list":
return UserFavDetailSerializer
elif self.action == "create":
return UserFavSerializer
return UserFavSerializer

用户留言功能

user_operation/serializers.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class LeavingMessageSerializer(serializers.ModelSerializer):
'''
用户留言
'''
# 获取当前登录的用户
user = serializers.HiddenField(
default=serializers.CurrentUserDefault()
)
# read_only:只返回,post时候可以不用提交,format:格式化输出
add_time = serializers.DateTimeField(read_only=True, format='%Y-%m-%d %H:%M')

class Meta:
model = UserLeavingMessage
fields = ("user", "message_type", "subject", "message", "file", "id", "add_time")

views.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class LeavingMessageViewset(mixins.ListModelMixin, mixins.DestroyModelMixin, mixins.CreateModelMixin,
viewsets.GenericViewSet):
"""
list:
获取用户留言
create:
添加留言
delete:
删除留言功能
"""

permission_classes = (IsAuthenticated, IsOwnerOrReadOnly)
authentication_classes = (JSONWebTokenAuthentication, SessionAuthentication)
serializer_class = LeavingMessageSerializer

# 只能看到自己的留言
def get_queryset(self):
return UserLeavingMessage.objects.filter(user=self.request.user)

urls.py

1
2
# 配置用户留言的url
router.register(r'messages', LeavingMessageViewset, base_name="messages")

用户收获地址

user_operation/serializers.py

1
2
3
4
5
6
7
8
9
class AddressSerializer(serializers.ModelSerializer):
user = serializers.HiddenField(
default=serializers.CurrentUserDefault()
)
add_time = serializers.DateTimeField(read_only=True, format='%Y-%m-%d %H:%M')

class Meta:
model = UserAddress
fields = ("id", "user", "province", "city", "district", "address", "signer_name", "add_time", "signer_mobile")

views.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class AddressViewset(viewsets.ModelViewSet):
"""
收货地址管理
list:
获取收货地址
create:
添加收货地址
update:
更新收货地址
delete:
删除收货地址
"""
permission_classes = (IsAuthenticated, IsOwnerOrReadOnly)
authentication_classes = (JSONWebTokenAuthentication, SessionAuthentication)
serializer_class = AddressSerializer

def get_queryset(self):
return UserAddress.objects.filter(user=self.request.user)

urls.py

1
2
# 配置收货地址
router.register(r'address',AddressViewset , base_name="address")

购物车、订单管理和远程调试

添加商品到购物车

trade/serializers.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
from rest_framework import serializers

from goods.models import Goods
from trade.models import ShoppingCart


class ShopCartSerializer(serializers.Serializer):
# 获取当前登录的用户
user = serializers.HiddenField(
default=serializers.CurrentUserDefault()
)
nums = serializers.IntegerField(required=True, label="数量", min_value=1,
error_messages={
"min_value": "商品数量不能小于一",
"required": "请选择购买数量"
})
# 这里是继承Serializer,必须指定queryset对象,如果继承ModelSerializer则不需要指定
# goods是一个外键,可以通过这方法获取goods object中所有的值
goods = serializers.PrimaryKeyRelatedField(required=True, queryset=Goods.objects.all())

# 继承的Serializer没有save功能,必须写一个create方法
def create(self, validated_data):
# validated_data是已经处理过的数据
# 获取当前用户
# view中:self.request.user;serizlizer中:self.context["request"].user
user = self.context["request"].user
nums = validated_data["nums"]
goods = validated_data["goods"]

existed = ShoppingCart.objects.filter(user=user, goods=goods)
# 如果购物车中有记录,数量+1
# 如果购物车车没有记录,就创建
if existed:
existed = existed[0]
existed.nums += nums
existed.save()
else:
# 添加到购物车
existed = ShoppingCart.objects.create(**validated_data)

return existed

trade/views.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
from rest_framework import viewsets
from rest_framework.authentication import SessionAuthentication
from rest_framework.permissions import IsAuthenticated
from rest_framework_jwt.authentication import JSONWebTokenAuthentication

from trade.models import ShoppingCart
from trade.serializers import ShopCartSerializer
from utils.permissions import IsOwnerOrReadOnly


class ShoppingCartViewset(viewsets.ModelViewSet):
"""
购物车功能
list:
获取购物车详情
create:
加入购物车
delete:
删除购物记录
"""
permission_classes = (IsAuthenticated, IsOwnerOrReadOnly)
authentication_classes = (JSONWebTokenAuthentication, SessionAuthentication)

serializer_class = ShopCartSerializer

def get_queryset(self):
return ShoppingCart.objects.filter(user=self.request.user)

urls.py

1
2
# 配置购物车的url
router.register(r'shopcarts', ShoppingCartViewset, base_name="shopcarts")

修改购物车数量

Serializer继承BaseSerializer,但是Seriazer中并没有重新update方法,所有添加一个update方法

trade/serializers.py

1
2
3
4
5
def update(self, instance, validated_data):
# 修改商品数量
instance.nums = validated_data["nums"]
instance.save()
return instance

在view中要把商品id传过去

1
lookup_field = "goods_id"

vue和购物车接口联调

trade/serializers.py

1
2
3
4
5
6
7
8
9
class ShopCartDetailSerializer(serializers.ModelSerializer):
'''
购物车商品详情信息
'''
# 一个购物车对应一个商品
goods = GoodsSerializer(many=False, read_only=True)
class Meta:
model = ShoppingCart
fields = ("goods", "nums")

trade/views.py
需要动态选择serializer

1
2
3
4
5
def get_serializer_class(self):
if self.action == 'list':
return ShopCartDetailSerializer
else:
return ShopCartSerializer

订单管理接口

用户添加商品到购物车,点去购物车结算,填上地址留言,结算生成订单,在会员中心我的订单里面,可以看到订单列表,点订单可以看到订单的详细信息.

trade/serializers.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
# 订单中的商品
class OrderGoodsSerialzier(serializers.ModelSerializer):
goods = GoodsSerializer(many=False)

class Meta:
model = OrderGoods
fields = "__all__"


# 订单商品信息
# goods字段需要嵌套一个OrderGoodsSerializer
class OrderDetailSerializer(serializers.ModelSerializer):
goods = OrderGoodsSerialzier(many=True)

class Meta:
model = OrderInfo
fields = "__all__"


class OrderSerializer(serializers.ModelSerializer):
user = serializers.HiddenField(
default=serializers.CurrentUserDefault()
)
# 生成订单的时候这些不用post
pay_status = serializers.CharField(read_only=True)
trade_no = serializers.CharField(read_only=True)
order_sn = serializers.CharField(read_only=True)
pay_time = serializers.DateTimeField(read_only=True)
nonce_str = serializers.CharField(read_only=True)
pay_type = serializers.CharField(read_only=True)

def generate_order_sn(self):
# 生成订单号
# 当前时间+userid+随机数
from random import Random
random_ins = Random()
order_sn = "{time_str}{userid}{ranstr}".format(time_str=time.strftime("%Y%m%d%H%M%S"),
userid=self.context["request"].user.id,
ranstr=random_ins.randint(10, 99))
return order_sn

def validate(self, attrs):
# validate中添加order_sn,然后在view中就可以save
attrs["order_sn"] = self.generate_order_sn()
return attrs

class Meta:
model = OrderInfo
fields = "__all__"

trade/views.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
class OrderViewset(mixins.ListModelMixin, mixins.RetrieveModelMixin, mixins.CreateModelMixin, mixins.DestroyModelMixin,
viewsets.GenericViewSet):
"""
订单管理
list:
获取个人订单
delete:
删除订单
create:
新增订单
"""
permission_classes = (IsAuthenticated, IsOwnerOrReadOnly)
authentication_classes = (JSONWebTokenAuthentication, SessionAuthentication)
serializer_class = OrderSerializer

# 动态配置serializer
def get_serializer_class(self):
if self.action == "retrieve":
return OrderDetailSerializer
return OrderSerializer

# 获取订单列表
def get_queryset(self):
return OrderInfo.objects.filter(user=self.request.user)

# 在订单提交保存之前还需要多两步步骤,所以这里自定义perform_create方法
# 1.将购物车中的商品保存到OrderGoods中
# 2.清空购物车
def perform_create(self, serializer):
order = serializer.save()
# 获取购物车所有商品
shop_carts = ShoppingCart.objects.filter(user=self.request.user)
for shop_cart in shop_carts:
order_goods = OrderGoods()
order_goods.goods = shop_cart.goods
order_goods.goods_num = shop_cart.nums
order_goods.order = order
order_goods.save()
# 清空购物车
shop_cart.delete()
return order

urls.py

1
2
# 配置订单的url
router.register(r'orders', OrderViewset, base_name="orders")

远程调试

第三方登录和支付,都需要有服务器(公网IP),我们可以用pycharm去远程调试服务器代码

配置安全组,开启ssh,数据传输等可以参考之前的部署

支付宝沙箱环境配置

https://openhome.alipay.com/platform/appDaily.htm?tab=info

使用支付宝账号进行登陆

RSA私钥及公钥生成

https://docs.open.alipay.com/291/105971/
下载工具,按文档提示使用

在trade文件夹下新建keys文件夹,新建public.txtprivate.txt两个文件
trade/keys/private.txt

1
2
3
-----BEGIN RSA PRIVATE KEY-----
MIIEpAIBAAKCAQEAvRYNqz/3Ye+ewQeRfvLXuAu5N/+bdC3KBj8L63j
-----END RSA PRIVATE KEY-----

trade/keys/public.txt

1
2
3
-----BEGIN RSA PRIVATE KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvRYN
-----END RSA PRIVATE KEY-----

中间的字符串是工具中生成的公钥和私钥(应用公钥)
将应用公钥上传到沙箱应用中

在新建一个alipay_key.txt文件
trade/keys/alipay_key.txt

1
2
3
-----BEGIN RSA PRIVATE KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCA
-----END RSA PRIVATE KEY-----

中间的是支付宝公钥

支付文档说明
https://docs.open.alipay.com/270

代码实现

首先的安装一个模块

1
pip install pycryptodome

utils中新建alipay.py

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

from Crypto.Signature import PKCS1_v1_5
from Crypto.Hash import SHA256
from base64 import b64encode, b64decode
from urllib.parse import quote_plus
from urllib.parse import urlparse, parse_qs
from urllib.request import urlopen
from base64 import decodebytes, encodebytes

import json


class AliPay(object):
"""
支付宝支付接口
"""

def __init__(self, appid, app_notify_url, app_private_key_path,
alipay_public_key_path, return_url, debug=False):
self.appid = appid
self.app_notify_url = app_notify_url
# 私钥
self.app_private_key_path = app_private_key_path
self.app_private_key = None
self.return_url = return_url
with open(self.app_private_key_path) as fp:
self.app_private_key = RSA.importKey(fp.read())
# 公钥
self.alipay_public_key_path = alipay_public_key_path
with open(self.alipay_public_key_path) as fp:
self.alipay_public_key = RSA.import_key(fp.read())

if debug is True:
self.__gateway = "https://openapi.alipaydev.com/gateway.do"
else:
self.__gateway = "https://openapi.alipay.com/gateway.do"

def direct_pay(self, subject, out_trade_no, total_amount, return_url=None, **kwargs):
# 请求参数
biz_content = {
"subject": subject,
"out_trade_no": out_trade_no,
"total_amount": total_amount,
"product_code": "FAST_INSTANT_TRADE_PAY",
# "qr_pay_mode":4
}
# 允许传递更多参数,放到biz_content
biz_content.update(kwargs)
data = self.build_body("alipay.trade.page.pay", biz_content, self.return_url)
return self.sign_data(data)

def build_body(self, method, biz_content, return_url=None):
# build_body主要生产消息的格式
# 公共请求参数
data = {
"app_id": self.appid,
"method": method,
"charset": "utf-8",
"sign_type": "RSA2",
"timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
"version": "1.0",
"biz_content": biz_content
}

if return_url is not None:
data["notify_url"] = self.app_notify_url
data["return_url"] = self.return_url

return data

def sign_data(self, data):
# 签名
data.pop("sign", None)
# 排序后的字符串
unsigned_items = self.ordered_data(data)
# 排完序后拼接起来
unsigned_string = "&".join("{0}={1}".format(k, v) for k, v in unsigned_items)
# 这里得到签名的字符串
sign = self.sign(unsigned_string.encode("utf-8"))
# 对url进行处理
quoted_string = "&".join("{0}={1}".format(k, quote_plus(v)) for k, v in unsigned_items)

# 获得最终的订单信息字符串
signed_string = quoted_string + "&sign=" + quote_plus(sign)
return signed_string

# 参数传进来一定要排序
def ordered_data(self, data):
complex_keys = []
for key, value in data.items():
if isinstance(value, dict):
complex_keys.append(key)

# 将字典类型的数据dump出来
for key in complex_keys:
data[key] = json.dumps(data[key], separators=(',', ':'))

return sorted([(k, v) for k, v in data.items()])

def sign(self, unsigned_string):
# 开始计算签名
key = self.app_private_key
# 签名的对象
signer = PKCS1_v1_5.new(key)
# 生成签名
signature = signer.sign(SHA256.new(unsigned_string))
# base64 编码,转换为unicode表示并移除回车
sign = encodebytes(signature).decode("utf8").replace("\n", "")
return sign

def _verify(self, raw_content, signature):
# 开始计算签名
key = self.alipay_public_key
signer = PKCS1_v1_5.new(key)
digest = SHA256.new()
digest.update(raw_content.encode("utf8"))
if signer.verify(digest, decodebytes(signature.encode("utf8"))):
return True
return False

def verify(self, data, signature):
if "sign_type" in data:
sign_type = data.pop("sign_type")
# 排序后的字符串
unsigned_items = self.ordered_data(data)
message = "&".join(u"{}={}".format(k, v) for k, v in unsigned_items)
return self._verify(message, signature)


if __name__ == "__main__":
return_url = 'http://127.0.0.1:8000/?total_amount=100.00&timestamp=2017-08-15+23%3A53%3A34&sign=e9E9UE0AxR84NK8TP1CicX6aZL8VQj68ylugWGHnM79zA7BKTIuxxkf%2FvhdDYz4XOLzNf9pTJxTDt8tTAAx%2FfUAJln4WAeZbacf1Gp4IzodcqU%2FsIc4z93xlfIZ7OLBoWW0kpKQ8AdOxrWBMXZck%2F1cffy4Ya2dWOYM6Pcdpd94CLNRPlH6kFsMCJCbhqvyJTflxdpVQ9kpH%2B%2Fhpqrqvm678vLwM%2B29LgqsLq0lojFWLe5ZGS1iFBdKiQI6wZiisBff%2BdAKT9Wcao3XeBUGigzUmVyEoVIcWJBH0Q8KTwz6IRC0S74FtfDWTafplUHlL%2Fnf6j%2FQd1y6Wcr2A5Kl6BQ%3D%3D&trade_no=2017081521001004340200204115&sign_type=RSA2&auth_app_id=2016080600180695&charset=utf-8&seller_id=2088102170208070&method=alipay.trade.page.pay.return&app_id=2016080600180695&out_trade_no=20170202185&version=1.0'
o = urlparse(return_url)
query = parse_qs(o.query)
processed_query = {}
ali_sign = query.pop("sign")[0]

# 测试用例
alipay = AliPay(
# 沙箱里面的appid值
appid="2016091500517596",
# notify_url是异步的url
app_notify_url="http://127.0.0.1:8000/",
# 我们自己商户的密钥
app_private_key_path="../trade/keys/private.txt",
# 支付宝的公钥
alipay_public_key_path="../trade/keys/alipay_key.txt", # 支付宝的公钥,验证支付宝回传消息使用,不是你自己的公钥,
# debug为true时使用沙箱的url.如果不是用正式环境的url
debug=True, # 默认False,
return_url="http://127.0.0.1:8000/alipay/return/"
)

for key, value in query.items():
processed_query[key] = value[0]
# print (alipay.verify(processed_query, ali_sign))

# 直接支付:生成请求的字符串.
url = alipay.direct_pay(
# 订单标题
subject="测试订单",
# 我们商户自行生成的订单号
out_trade_no="201809270840",
# 订单金额
total_amount=100,
# 成功付款后跳转到的页面,return_url同步的url
# return_url="http://127.0.0.1:8000/"
)
# 将生成的请求字符串拿到我们的url中进行拼接
re_url = "https://openapi.alipaydev.com/gateway.do?{data}".format(data=url)

print(re_url)

运行这个文件,点击生成的链接,就跳转到了支付界面

django集成支付宝

urls.py

1
2
# 配置支付宝支付相关接口的url
path('alipay/return/', AlipayView.as_view())

alipay.py

1
2
3
4
5
# 把return_url和notify_url都改成远程服务器的地址

return_url="http://xxx.xxx.xxx.xxx:8000/alipay/return/"

app_notify_url="http://xxx.xxx.xxx.xxx:8000/alipay/return/"

settings.py中配置公钥私钥路径

1
2
3
# 支付宝相关的key
private_key_path = os.path.join(BASE_DIR, 'apps/trade/keys/private.txt')
ali_pub_key_path = os.path.join(BASE_DIR, 'apps/trade/keys/alipay_key.txt')

trade/views.py

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


class AlipayView(APIView):
def get(self, request):
"""
处理支付宝的return_url返回
"""
processed_dict = {}
# 1. 获取GET中参数
for key, value in request.GET.items():
processed_dict[key] = value
# 2. 取出sign
sign = processed_dict.pop("sign", None)

# 3. 生成ALipay对象
alipay = AliPay(
appid="201609xxxx456",
app_notify_url="http://xxx.xxx.xxx.xxx:8000/alipay/return/",
app_private_key_path=private_key_path,
alipay_public_key_path=ali_pub_key_path, # 支付宝的公钥,验证支付宝回传消息使用,不是你自己的公钥,
debug=True, # 默认False,
return_url="http://xxx.xxx.xxx.xxx:8000/alipay/return/"
)

verify_re = alipay.verify(processed_dict, sign)

# 这里可以不做操作.因为不管发不发return url.notify url都会修改订单状态.
if verify_re is True:
order_sn = processed_dict.get('out_trade_no', None)
trade_no = processed_dict.get('trade_no', None)
trade_status = processed_dict.get('trade_status', None)

existed_orders = OrderInfo.objects.filter(order_sn=order_sn)
for existed_order in existed_orders:
existed_order.pay_status = trade_status
existed_order.trade_no = trade_no
existed_order.pay_time = datetime.now()
existed_order.save()

def post(self, request):
"""
处理支付宝的notify_url
"""
# 存放post里面所有的数据
processed_dict = {}
# 取出post里面的数据
for key, value in request.POST.items():
processed_dict[key] = value
# 把signpop掉,文档有说明
sign = processed_dict.pop("sign", None)

# 生成一个Alipay对象
alipay = AliPay(
appid="201609xxxx456",
app_notify_url="http://xxx.xxx.xxx.xxx:8000/alipay/return/",
app_private_key_path=private_key_path,
alipay_public_key_path=ali_pub_key_path, # 支付宝的公钥,验证支付宝回传消息使用,不是你自己的公钥,
debug=True, # 默认False,
return_url="http://xxx.xxx.xxx.xxx:8000/alipay/return/"
)

# 进行验证
verify_re = alipay.verify(processed_dict, sign)

# 如果验签成功
if verify_re is True:
# 商户网站唯一订单号
order_sn = processed_dict.get('out_trade_no', None)
# 支付宝系统交易流水号
trade_no = processed_dict.get('trade_no', None)
# 交易状态
trade_status = processed_dict.get('trade_status', None)

# 查询数据库中订单记录
existed_orders = OrderInfo.objects.filter(order_sn=order_sn)
for existed_order in existed_orders:
# 订单商品项
order_goods = existed_order.goods.all()
# 商品销量增加订单中数值
for order_good in order_goods:
goods = order_good.goods
goods.sold_num += order_good.goods_num
goods.save()

# 更新订单状态
existed_order.pay_status = trade_status
existed_order.trade_no = trade_no
existed_order.pay_time = datetime.now()
existed_order.save()
# 需要返回一个'success'给支付宝,如果不返回,支付宝会一直发送订单支付成功的消息
return Response("success")

trade/serializers.py

创建订单的时候生成一个支付的url,这个逻辑OderSerializerOrderDetailSerializer中都添加

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#支付订单的url
alipay_url = serializers.SerializerMethodField(read_only=True)

def get_alipay_url(self, obj):
alipay = AliPay(
appid="20160xxxx17456",
app_notify_url="http://4xx.xxx.xxx.xx59:8000/alipay/return/",
app_private_key_path=private_key_path,
alipay_public_key_path=ali_pub_key_path, # 支付宝的公钥,验证支付宝回传消息使用,不是你自己的公钥,
debug=True, # 默认False,
return_url="http://4xx.xxx.xxx.xx59:8000/alipay/return/"
)

url = alipay.direct_pay(
subject=obj.order_sn,
out_trade_no=obj.order_sn,
total_amount=obj.order_mount,
)
re_url = "https://openapi.alipaydev.com/gateway.do?{data}".format(data=url)

return re_url

服务器测试

把vue项目中api.js里面local_host改为服务器ip

1
let local_host = 'http://47.93.198.159:8000';

在pycharm中调用服务器的解释器运行代码

浏览器访问地址:http://47.93.198.159:8000/orders/

输入数据进行测试

进入返回的alipay的url进行测试

测试的时候发现,明明支付成功了,数据库中的状态却没了,
既不是paying,也不是TRADE_SUCCESS,就是一个空字符串
这是因为支付宝的同步跳转参数中没有支付状态,而我们却在get请求中去获取这个参数
这里需要将get函数中的获取交易状态删除(注释掉)
https://docs.open.alipay.com/270/alipay.trade.page.pay

1
# trade_status = processed_dict.get('trade_status', None)

vue静态文件放到django中

运行npm run build会生成一个dist目录,打开这个目录

把index.html拷贝到templates目录下
把index.entry.js考到django的static目录下面
把dist/static下的两个文件夹拷贝到static目录下
修改js路径

配置index的url

1
2
3
4
5
复制代码
from django.views.generic import TemplateView

# 首页
path('index/', TemplateView.as_view(template_name='index.html'),name='index')

配置支付成功return的地址

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
if verify_re is True:
order_sn = processed_dict.get('out_trade_no', None)
trade_no = processed_dict.get('trade_no', None)
# trade_status = processed_dict.get('trade_status', None)

existed_orders = OrderInfo.objects.filter(order_sn=order_sn)
for existed_order in existed_orders:
# existed_order.pay_status = trade_status
existed_order.trade_no = trade_no
existed_order.pay_time = datetime.now()
existed_order.save()

response = redirect("/index/#/app/home/member/order")
return response
else:
response = redirect("index")
return response

同步到服务器,浏览器访问进行测试
http://IP:8000/index/#/app/home/index

class AlipayView(APIView)

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


class AlipayView(APIView):
def get(self, request):
"""
处理支付宝的return_url返回
"""
processed_dict = {}
# 1. 获取GET中参数
for key, value in request.GET.items():
processed_dict[key] = value
# 2. 取出sign
sign = processed_dict.pop("sign", None)

# 3. 生成ALipay对象
alipay = AliPay(
appid="2016091500517596",
app_notify_url="http://120.79.65.88:8000/alipay/return/",
app_private_key_path=private_key_path,
alipay_public_key_path=ali_pub_key_path, # 支付宝的公钥,验证支付宝回传消息使用,不是你自己的公钥,
debug=True, # 默认False,
return_url="http://120.79.65.88:8000/alipay/return/"
)

verify_re = alipay.verify(processed_dict, sign)

# 这里可以不做操作.因为不管发不发return url.notify url都会修改订单状态.
if verify_re is True:
order_sn = processed_dict.get('out_trade_no', None)
trade_no = processed_dict.get('trade_no', None)
# trade_status = processed_dict.get('trade_status', None)

existed_orders = OrderInfo.objects.filter(order_sn=order_sn)
for existed_order in existed_orders:
# existed_order.pay_status = trade_status
existed_order.trade_no = trade_no
existed_order.pay_time = datetime.now()
existed_order.save()

response = redirect("/index/#/app/home/member/order")
return response
else:
response = redirect("index")
return response

def post(self, request):
"""
处理支付宝的notify_url
"""
# 存放post里面所有的数据
processed_dict = {}
# 取出post里面的数据
for key, value in request.POST.items():
processed_dict[key] = value
# 把signpop掉,文档有说明
sign = processed_dict.pop("sign", None)

# 生成一个Alipay对象
alipay = AliPay(
appid="2016091500517596",
app_notify_url="http://120.79.65.88:8000/alipay/return/",
app_private_key_path=private_key_path,
alipay_public_key_path=ali_pub_key_path, # 支付宝的公钥,验证支付宝回传消息使用,不是你自己的公钥,
debug=True, # 默认False,
return_url="http://120.79.65.88:8000/alipay/return/"
)

# 进行验证
verify_re = alipay.verify(processed_dict, sign)

# 如果验签成功
if verify_re is True:
# 商户网站唯一订单号
order_sn = processed_dict.get('out_trade_no', None)
# 支付宝系统交易流水号
trade_no = processed_dict.get('trade_no', None)
# 交易状态
trade_status = processed_dict.get('trade_status', None)

# 查询数据库中订单记录
existed_orders = OrderInfo.objects.filter(order_sn=order_sn)
for existed_order in existed_orders:
# 订单商品项
order_goods = existed_order.goods.all()
# 商品销量增加订单中数值
for order_good in order_goods:
goods = order_good.goods
goods.sold_num += order_good.goods_num
goods.save()

# 更新订单状态
existed_order.pay_status = trade_status
existed_order.trade_no = trade_no
existed_order.pay_time = datetime.now()
existed_order.save()
# 需要返回一个'success'给支付宝,如果不返回,支付宝会一直发送订单支付成功的消息
return Response("success")

首页、商品数量、缓存和限速功能开发

将环境切换为本地,vue也切换为本地

轮播图

goods/serializers.py

1
2
3
4
5
6
7
8
class BannerSerializer(serializers.ModelSerializer):
'''
轮播图
'''

class Meta:
model = Banner
fields = "__all__"

goods/views.py

1
2
3
4
5
6
class BannerViewset(mixins.ListModelMixin, viewsets.GenericViewSet):
"""
首页轮播图
"""
queryset = Banner.objects.all().order_by("index")
serializer_class = BannerSerializer

urls.py

1
2
# 配置首页轮播图的url
router.register(r'banners', BannerViewset, base_name="banners")

新品推荐功能

在设计Goods model时候有一个字段is_new

1
is_new = models.BooleanField(default=False, verbose_name="是否新品")

实现这个接口只要在goods/filters/GoodsFilter里面添加一个过滤就可以了

1
2
3
class Meta:
model = Goods
fields = ['pricemin', 'pricemax','is_hot','is_new']

首页商品分类显示功能

goods/serializers.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
class BrandSerializer(serializers.ModelSerializer):
'''
大类下面的宣传商标
'''

class Meta:
model = GoodsCategoryBrand
fields = "__all__"


class IndexCategorySerializer(serializers.ModelSerializer):
# 某个大类的商标,可以有多个商标,一对多的关系
brands = BrandSerializer(many=True)
# good有一个外键category,但这个外键指向的是三级类,直接反向通过外键category(三级类),取某个大类下面的商品是取不出来的
goods = serializers.SerializerMethodField()
# 在parent_category字段中定义的related_name="sub_cat"
# 取二级商品分类
sub_cat = CategorySerializer2(many=True)
# 广告商品
ad_goods = serializers.SerializerMethodField()

def get_ad_goods(self, obj):
goods_json = {}
ad_goods = IndexAd.objects.filter(category_id=obj.id, )
if ad_goods:
# 取到这个商品Queryset[0]
good_ins = ad_goods[0].goods
# 在serializer里面调用serializer的话,就要添加一个参数context(上下文request),
# 否则图片链接是不完整的
# 嵌套serializer必须加
# serializer返回的时候一定要加 “.data” ,这样才是json数据
goods_json = GoodsSerializer(good_ins, many=False, context={'request': self.context['request']}).data
return goods_json

# 自定义获取方法
def get_goods(self, obj):
# 将这个商品相关父类子类等都可以进行匹配
all_goods = Goods.objects.filter(Q(category_id=obj.id) | Q(category__parent_category_id=obj.id) | Q(
category__parent_category__parent_category_id=obj.id))
goods_serializer = GoodsSerializer(all_goods, many=True, context={'request': self.context['request']})
return goods_serializer.data

class Meta:
model = GoodsCategory
fields = "__all__"

goods/views.py

1
2
3
4
5
6
7
class IndexCategoryViewset(mixins.ListModelMixin, viewsets.GenericViewSet):
"""
首页商品分类数据
"""
# 获取is_tab=True(导航栏)里面的分类下的商品数据
queryset = GoodsCategory.objects.filter(is_tab=True, name__in=["生鲜食品", "酒水饮料"])
serializer_class = IndexCategorySerializer

urls.py

1
2
# 首页系列商品展示url
router.register(r'indexgoods', IndexCategoryViewset, base_name="indexgoods")

热搜词

goods/serializers.py

1
2
3
4
class HotWordsSerializer(serializers.ModelSerializer):
class Meta:
model = HotSearchWords
fields = "__all__"

goods/views.py

1
2
3
4
5
6
class HotSearchsViewset(mixins.ListModelMixin, viewsets.GenericViewSet):
"""
获取热搜词列表
"""
queryset = HotSearchWords.objects.all().order_by("-index")
serializer_class = HotWordsSerializer

urls.py

1
2
# 首页热搜词
router.register(r'hotsearchs', HotSearchsViewset, base_name="hotsearchs")

商品点击数和收藏数

GoodsListViewSet其中继承了mixins.RetrieveModelMixin
只需要重写retrieve方法即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class GoodsListViewSet(mixins.ListModelMixin, mixins.RetrieveModelMixin, viewsets.GenericViewSet):
'''
商品列表页, 分页, 过滤, 排序
'''
queryset = Goods.objects.all().order_by('id')
serializer_class = GoodsSerializer
pagination_class = GoodsPagination
filter_backends = (DjangoFilterBackend, SearchFilter, OrderingFilter)

# authentication_classes = (TokenAuthentication,)
# 自定义过滤器
filter_class = GoodsFilter
# 搜索,默认模糊查询
search_fields = ('name', 'goods_brief')
# 排序
ordering_fields = ('sold_num', 'shop_price')

# 商品点击数 + 1
def retrieve(self, request, *args, **kwargs):
instance = self.get_object()
instance.click_num += 1
instance.save()
serializer = self.get_serializer(instance)
return Response(serializer.data)

UserFavViewset继承了mixins.CreateModelMixin
重写perform_create方法即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
class UserFavViewset(viewsets.GenericViewSet, mixins.ListModelMixin, mixins.CreateModelMixin, mixins.DestroyModelMixin):
'''
list:
获取用户的所有收藏
create:
添加收藏
destroy:
取消收藏
'''
# permission是用来做权限判断的
# IsAuthenticated:必须登录用户;IsOwnerOrReadOnly:必须是当前登录的用户
permission_classes = (IsAuthenticated, IsOwnerOrReadOnly)
# auth使用来做用户认证的
authentication_classes = (JSONWebTokenAuthentication, SessionAuthentication)
# 搜索的字段
lookup_field = 'goods_id'

def get_queryset(self):
# 只能查看当前登录用户的收藏,不会获取所有用户的收藏
return UserFav.objects.filter(user=self.request.user)

# 动态选择serializer
def get_serializer_class(self):
if self.action == "list":
return UserFavDetailSerializer
elif self.action == "create":
return UserFavSerializer
return UserFavSerializer

# 用户收藏的商品数量+1
def perform_create(self, serializer):
instance = serializer.save()
# 这里instance相当于UserFav model,通过它找到goods
goods = instance.goods
goods.fav_num += 1
goods.save()

用信号量实现收藏数变化
delete和create的时候django model都会发送一个信号量出来,用信号量的方式代码分离性更好

user_operation/signals.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
from django.db.models.signals import post_delete, post_save
from django.dispatch import receiver

from user_operation.models import UserFav


# post_save : model变化方式
# sender : 变动的model
@receiver(post_save, sender=UserFav)
def create_userfav(sender, instance=None, created=False, **kwargs):
if created:
goods = instance.goods
goods.fav_num += 1
goods.save()


@receiver(post_delete, sender=UserFav)
def delete_userfav(sender, instance=None, created=False, **kwargs):
goods = instance.goods
goods.fav_num -= 1
goods.save()

user_operation/apps.py

1
2
3
4
5
6
7
8
9
from django.apps import AppConfig


class UserOperationConfig(AppConfig):
name = 'user_operation'
verbose_name = "用户操作管理"

def ready(self):
import user_operation.signals

商品库存和销量修改

引起商品库存数量变化的行为:

  • 新增商品到购物车
  • 修改购物车数量
  • 删除购物车记录

trade/views.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
class ShoppingCartViewset(viewsets.ModelViewSet):
"""
购物车功能
list:
获取购物车详情
create:
加入购物车
delete:
删除购物记录
"""
permission_classes = (IsAuthenticated, IsOwnerOrReadOnly)
authentication_classes = (JSONWebTokenAuthentication, SessionAuthentication)

# serializer_class = ShopCartSerializer
lookup_field = "goods_id"

def get_queryset(self):
return ShoppingCart.objects.filter(user=self.request.user)

def get_serializer_class(self):
if self.action == 'list':
return ShopCartDetailSerializer
else:
return ShopCartSerializer

# 库存数-n
def perform_create(self, serializer):
shop_cart = serializer.save()
goods = shop_cart.goods
goods.goods_num -= shop_cart.nums
goods.save()

# 库存数+n
def perform_destroy(self, instance):
goods = instance.goods
goods.goods_num += instance.nums
goods.save()
instance.delete()

# 更新库存,修改可能是增加页可能是减少
def perform_update(self, serializer):
# 首先获取修改之前的库存数量
existed_record = ShoppingCart.objects.get(id=serializer.instance.id)
existed_nums = existed_record.nums
# 先保存之前的数据existed_nums
saved_record = serializer.save()
# 变化的数量
nums = saved_record.nums - existed_nums
goods = saved_record.goods
goods.goods_num -= nums
goods.save()

商品的销量只有在支付成功后才会 +n
trade/views.py
AlipayView/post方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 查询数据库中订单记录
existed_orders = OrderInfo.objects.filter(order_sn=order_sn)
for existed_order in existed_orders:
# 订单商品项
order_goods = existed_order.goods.all()
# 商品销量增加订单中数值
for order_good in order_goods:
goods = order_good.goods
goods.sold_num += order_good.goods_num
goods.save()

# 更新订单状态
existed_order.pay_status = trade_status
existed_order.trade_no = trade_no
existed_order.pay_time = datetime.now()
existed_order.save()
# 需要返回一个'success'给支付宝,如果不返回,支付宝会一直发送订单支付成功的消息
return Response("success")

drf缓存

http://chibisov.github.io/drf-extensions/docs/#caching

1
pip install drf-extensions

简单使用
在GoodsListViewSet中添加缓存功能

1
2
3
4
5
from rest_framework_extensions.cache.mixins import CacheResponseMixin

# CacheResponseMixin一定要放在第一个位置

class GoodsListViewSet(CacheResponseMixin,mixins.ListModelMixin, mixins.RetrieveModelMixin,viewsets.GenericViewSet):

设置过期时间,settings里面

1
2
3
4
# 缓存配置
REST_FRAMEWORK_EXTENSIONS = {
'DEFAULT_CACHE_RESPONSE_TIMEOUT': 60*60*8 # 多少秒过期,时间自己可以随便设定
}

这个缓存使用的是内存,每次重启之后就会失效

drf配置redis缓存

https://django-redis-chs.readthedocs.io/zh_CN/latest/

drf的throttle设置api的访问速率

针对爬虫
http://www.django-rest-framework.org/api-guide/throttling/

settings中配置

1
2
3
4
5
6
7
8
9
10
11
REST_FRAMEWORK = {
# 限速设置
'DEFAULT_THROTTLE_CLASSES': (
'rest_framework.throttling.AnonRateThrottle', # 未登陆用户
'rest_framework.throttling.UserRateThrottle' # 登陆用户
),
'DEFAULT_THROTTLE_RATES': {
'anon': '10/minute', # 每分钟可以请求n次
'user': '30/minute' # 每分钟可以请求n次
}
}

goods/views.py中使用

1
2
3
4
5
6
from rest_framework.throttling import UserRateThrottle,AnonRateThrottle

class GoodsListViewSet(CacheResponseMixin,mixins.ListModelMixin, mixins.RetrieveModelMixin,viewsets.GenericViewSet):
  .
  .
  throttle_classes = (UserRateThrottle, AnonRateThrottle)

第三方登录

微博创建应用,修改回调地址
http://open.weibo.com/authentication

安装第三方登录插件
https://github.com/python-social-auth/social-app-django

1
pip install social-auth-app-django

INSTALL_APP中配置

1
'social_django',

生成表

1
python manage.py migrate

添加到AUTHENTICATION_BACKENDS
settings.py

1
2
3
4
5
6
7
8
# 设置邮箱和用户名和手机号均可登录
AUTHENTICATION_BACKENDS = (
'users.views.CustomBackend',
'social_core.backends.weibo.WeiboOAuth2',
'social_core.backends.qq.QQOAuth2',
'social_core.backends.weixin.WeixinOAuth2',
'django.contrib.auth.backends.ModelBackend',
)

配置context_processors

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [os.path.join(BASE_DIR, 'templates')]
,
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
# 第三方登录
'social_django.context_processors.backends',
'social_django.context_processors.login_redirect',
],
},
},
]

APP Secret和App key配置,settings里面

1
2
3
4
5
6
7
8
9
# 第三方登录,里面的值是你的开放平台对应的值
SOCIAL_AUTH_WEIBO_KEY = 'xxxxxxx'
SOCIAL_AUTH_WEIBO_SECRET = 'xxxxxx'

SOCIAL_AUTH_QQ_KEY = 'xxxxxxx'
SOCIAL_AUTH_QQ_SECRET = 'xxxxxxx'

SOCIAL_AUTH_WEIXIN_KEY = 'xxxxxxx'
SOCIAL_AUTH_WEIXIN_SECRET = 'xxxxxxx'

1
2
# 登录成功后跳转到首页
SOCIAL_AUTH_LOGIN_REDIRECT_URL = '/index/'

登录成功跳到首页,发现还处于未登录状态,我们需要对源码做修改
将social_core拷贝到extra_apps目录下
social_core/actions.py
原始代码

1
return backend.strategy.redirect(url)

修改为

1
2
3
4
5
6
# 修改源码适配drf
response = backend.strategy.redirect(url)
payload = jwt_payload_handler(user)
response.set_cookie("name",user.name if user.name else user.username, max_age=24*3600)
response.set_cookie("token", jwt_encode_handler(payload), max_age=24*3600)
return response

现在就登录后就正常了.qq和微信的登录,一样的操作,只要去开放平台注册应用,其它跟微博登录一样设置就可以了

-------------本文结束感谢您的阅读-------------
坚持原创技术分享,您的支持将鼓励我继续创作!
0%