电商概念: SPU 和 SKU
- SPU(Standard Product Unit): 标准产品单位
- 可以理解为: 就是'类对象'
- 比如'iPhone X'
- SKU(Standard Keeping Unit): 库存量单位
- 可以理解为: 就是'类实例对象'
- 比如'iPhone X 黑色', 'iPhone X 白色'
商品部分
-
新建两个app
-
goods(商品)
-
contents(广告)
-
-
模型如下:
# goods.models
from django.db import models
from utils.models import BaseModel
class GoodsCategory(BaseModel):
"""
- 商品类别(分组)
- 1个自关联外键parent
- 1个必传字段: name
"""
name = models.CharField(max_length=10, verbose_name='名称')
parent = models.ForeignKey('self', null=True, blank=True, on_delete=models.CASCADE, verbose_name='父类别')
class Meta:
db_table = 'tb_goods_category'
verbose_name = '商品类别'
verbose_name_plural = verbose_name
def __str__(self):
return self.name
class GoodsChannel(BaseModel):
"""
- 商品频道
- 1个外键category关联GoodsCategory
- 4个必传字段: group_id,category,url,sequence
"""
group_id = models.IntegerField(verbose_name='组号')
category = models.ForeignKey(GoodsCategory, on_delete=models.CASCADE, verbose_name='顶级商品类别')
url = models.CharField(max_length=50, verbose_name='频道页面链接')
sequence = models.IntegerField(verbose_name='组内顺序')
class Meta:
db_table = 'tb_goods_channel'
verbose_name = '商品频道'
verbose_name_plural = verbose_name
def __str__(self):
return self.category.name
class Brand(BaseModel):
"""
- 品牌
- 没有外键
- 3个必传字段: group_id,category,url,sequence
"""
name = models.CharField(max_length=20, verbose_name='名称')
logo = models.ImageField(verbose_name='Logo图片')
first_letter = models.CharField(max_length=1, verbose_name='品牌首字母')
class Meta:
db_table = 'tb_brand'
verbose_name = '品牌'
verbose_name_plural = verbose_name
def __str__(self):
return self.name
class Goods(BaseModel):
"""
- 商品SPU
- 4个外键,其中3个外键对应GoodsCategory;最后1个外键对应Brand
- category1, category2, category3 对应GoodsCategory;
- brand 对应 Brand
- 5个必传字段: name,brand,category1,category2,category3
"""
name = models.CharField(max_length=50, verbose_name='名称')
brand = models.ForeignKey(Brand, on_delete=models.PROTECT, verbose_name='品牌')
category1 = models.ForeignKey(GoodsCategory, on_delete=models.PROTECT, related_name='cat1_goods', verbose_name='一级类别')
category2 = models.ForeignKey(GoodsCategory, on_delete=models.PROTECT, related_name='cat2_goods', verbose_name='二级类别')
category3 = models.ForeignKey(GoodsCategory, on_delete=models.PROTECT, related_name='cat3_goods', verbose_name='三级类别')
sales = models.IntegerField(default=0, verbose_name='销量')
comments = models.IntegerField(default=0, verbose_name='评价数')
class Meta:
db_table = 'tb_goods'
verbose_name = '商品'
verbose_name_plural = verbose_name
def __str__(self):
return self.name
class GoodsSpecification(BaseModel):
"""
- 商品规格
- 1个外键 goods 对应 Goods
- 2个必传字段: name,goods
"""
goods = models.ForeignKey(Goods, on_delete=models.CASCADE, verbose_name='商品')
name = models.CharField(max_length=20, verbose_name='规格名称')
class Meta:
db_table = 'tb_goods_specification'
verbose_name = '商品规格'
verbose_name_plural = verbose_name
def __str__(self):
return '%s: %s' % (self.goods.name, self.name)
class SpecificationOption(BaseModel):
"""
- 规格选项
- 1个外键 spec 对应 GoodsSpecification
- 2个必传字段: spec,value
"""
spec = models.ForeignKey(GoodsSpecification, on_delete=models.CASCADE, verbose_name='规格')
value = models.CharField(max_length=20, verbose_name='选项值')
class Meta:
db_table = 'tb_specification_option'
verbose_name = '规格选项'
verbose_name_plural = verbose_name
def __str__(self):
return '%s - %s' % (self.spec, self.value)
class SKU(BaseModel):
"""
- 商品SKU
- 2个外键,其中1个外键对应Goods;另一个1个外键对应GoodsCategory
- goods 对应 Goods
- category 对应 GoodsCategory
- 7个必传字段: name,caption,goods,category,price,cost_price,market_price
"""
name = models.CharField(max_length=50, verbose_name='名称')
caption = models.CharField(max_length=100, verbose_name='副标题')
goods = models.ForeignKey(Goods, on_delete=models.CASCADE, verbose_name='商品')
category = models.ForeignKey(GoodsCategory, on_delete=models.PROTECT, verbose_name='从属类别')
price = models.DecimalField(max_digits=10, decimal_places=2, verbose_name='单价')
cost_price = models.DecimalField(max_digits=10, decimal_places=2, verbose_name='进价')
market_price = models.DecimalField(max_digits=10, decimal_places=2, verbose_name='市场价')
stock = models.IntegerField(default=0, verbose_name='库存')
sales = models.IntegerField(default=0, verbose_name='销量')
comments = models.IntegerField(default=0, verbose_name='评价数')
is_launched = models.BooleanField(default=True, verbose_name='是否上架销售')
default_image_url = models.CharField(max_length=200, default='', null=True, blank=True, verbose_name='默认图片')
class Meta:
db_table = 'tb_sku'
verbose_name = '商品SKU'
verbose_name_plural = verbose_name
def __str__(self):
return '%s: %s' % (self.id, self.name)
class SKUImage(BaseModel):
"""
- SKU图片
- 1个外键 sku 对应 SKU
- 2个必传字段: sku,image
"""
sku = models.ForeignKey(SKU, on_delete=models.CASCADE, verbose_name='sku')
image = models.ImageField(verbose_name='图片')
class Meta:
db_table = 'tb_sku_image'
verbose_name = 'SKU图片'
verbose_name_plural = verbose_name
def __str__(self):
return '%s %s' % (self.sku.name, self.id)
class SKUSpecification(BaseModel):
"""
- SKU具体规格
- 3个外键
- sku 对应 SKU
- spec 对应 GoodsSpecification
- option 对应 SpecificationOption
- 3个必传字段: sku,spec,option
"""
sku = models.ForeignKey(SKU, on_delete=models.CASCADE, verbose_name='sku')
spec = models.ForeignKey(GoodsSpecification, on_delete=models.PROTECT, verbose_name='规格名称')
option = models.ForeignKey(SpecificationOption, on_delete=models.PROTECT, verbose_name='规格值')
class Meta:
db_table = 'tb_sku_specification'
verbose_name = 'SKU规格'
verbose_name_plural = verbose_name
def __str__(self):
return '%s: %s - %s' % (self.sku, self.spec.name, self.option.value)
# contents.models
from django.db import models
from utils.models import BaseModel
class ContentCategory(BaseModel):
"""
- 广告内容类别(分组)
- 没有外键
- 2个必传字段: name,key
"""
name = models.CharField(max_length=50, verbose_name='名称')
key = models.CharField(max_length=50, verbose_name='类别键名')
class Meta:
db_table = 'tb_content_category'
verbose_name = '广告内容类别'
verbose_name_plural = verbose_name
def __str__(self):
return self.name
class Content(BaseModel):
"""
- 广告内容(主表)
- 1个外键: category => ContentCategory
- 4个必传字段: category,title,url,sequence
"""
category = models.ForeignKey(ContentCategory, on_delete=models.PROTECT, verbose_name='类别')
title = models.CharField(max_length=100, verbose_name='标题')
url = models.CharField(max_length=300, verbose_name='内容链接')
image = models.ImageField(null=True, blank=True, verbose_name='图片')
text = models.TextField(null=True, blank=True, verbose_name='内容')
sequence = models.IntegerField(verbose_name='排序')
status = models.BooleanField(default=True, verbose_name='是否展示')
class Meta:
db_table = 'tb_content'
verbose_name = '广告内容'
verbose_name_plural = verbose_name
def __str__(self):
return self.category.name + ': ' + self.title
以下内容,参考网址
https://blog.csdn.net/weixin_44799217/article/details/118463124
FastDFS分布式文件系统
- 基于B/S 架构,工作流程详情见图
Docker组件
-
Docker客户端和服务器(或者称为'守护进程')
-
镜像:用户的应用程序,比如以前xp系统的'xxx.gho'
-
Registry(注册中心)相当于github的仓库,用来存放镜像
- 公有Registry和私有Registry
-
容器: 存放用户的应用程序/服务(是一个环境,'集装箱')
- 先启动容器,然后登录到容器,安装自己的应用
-
安装,参考网址,拷贝步骤如下
http://121.5.151.41/mylesson/Django/%E7%BE%8E%E5%A4%9A%E5%95%86%E5%9F%8E%E9%A1%B9%E7%9B%AE%E8%AF%BE%E4%BB%B6/C03-Goods/Docker/InstallAndOperations.html
- 更新ubuntu的apt源索引
sudo apt-get update
- 安装包允许apt通过HTTPS使用仓库
sudo apt-get install \
apt-transport-https \
ca-certificates \
curl \
software-properties-common
- 添加Docker官方GPG key
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -
- 设置Docker稳定版仓库
sudo add-apt-repository \
"deb [arch=amd64] https://download.docker.com/linux/ubuntu \
$(lsb_release -cs) \
stable"
- 添加仓库后,更新apt源索引
sudo apt-get update
- 安装最新版Docker CE(社区版)
sudo apt-get install docker-ce
- 检查Docker CE是否安装正确
sudo docker run hello-world
- 出现如下信息,表示安装成功
Hello from Docker!......
- (选做)为了避免每次命令都输入sudo,可以设置用户权限,注意执行后须注销重新登录
sudo usermod -a -G docker $USER
启动与停止
- 安装完成Docker后,默认已经启动了docker服务,如需手动控制docker服务的启停,可执行如下命令
# 启动docker
sudo service docker start
# 停止docker
sudo service docker stop
# 重启docker
sudo service docker restart
Docker镜像操作
-
Docker镜像: docker自己本身的底层文件,一般无需去关注
我们只操作 Docker镜像的'应用层',无需理会底层 -
image文件: 打包应用程序及其依赖
-
作用: 生成 docker 容器(实例)
-
同一个 image 文件,可以生成多个同时运行的容器实例
-
-
列出镜像
docker image ls
- 拉取镜像
- docker image pull library/hello-world
- docker image pull hello-world # 简写
- 删除镜像
docker image rm 镜像名或镜像id # docker image rm hello-world
- 注意事项:如果提示容器被占用,删除失败,可以这么解决,先删容器,再删镜像
- docker ps -a # 查看所有的容器(包括未运行的容器)
- docker rm 容器ID
- docker rm 镜像ID
Docker容器的操作
- 创建容器,把镜像扔进去并运行起来
- docker run [option] 镜像名 [向启动容器中传入的命令]
常用可选参数说明:
-i 表示以“交互模式”运行容器
-t 表示容器启动后会进入其命令行。加入这两个参数后,容器创建就能登录进去。即 分配一个伪终端。
--name 为创建的容器命名
-v 表示目录映射关系(前者是宿主机目录,后者是映射到宿主机上的目录,即 宿主机目录:容器中目录),可以使 用多个-v 做多个目录或文件映射。注意:最好做目录映射,在宿主机上做修改,然后 共享到容器上。
-d 在run后面加上-d参数,则会创建一个守护式容器在后台运行(这样创建容器后不 会自动登录容器,如果只加-i -t 两个参数,创建后就会自动进去容器)。
-p 表示端口映射,前者是宿主机端口,后者是容器内的映射端口。可以使用多个-p 做多个端口映射
-e 为容器设置环境变量
--network=host 表示将主机的网络环境映射到容器中,容器的网络与主机相同
- 交互式容器
# 创建一个交互式容器,并命名为myubuntu
docker run -it --name=myubuntu ubuntu /bin/bash
- 在容器中可以随意执行linux命令,就是一个ubuntu的环境,当执行exit命令退出时,该容器也随之停止
-
守护式容器
- 如果对于一个需要长期运行的容器来说,我们可以创建一个守护式容器
在容器内部exit退出时,容器也不会停止
- 如果对于一个需要长期运行的容器来说,我们可以创建一个守护式容器
docker run -dit --name=myubuntu2 ubuntu
- 进入已运行的容器
- docker exec -it 容器名或容器id 进入后执行的第一个命令
- docker exec -it myubuntu2 /bin/bash
- 查看容器
# 列出本机正在运行的容器
docker container ls
# 列出本机所有容器,包括已经终止运行的
docker container ls --all
- 停止与启动容器
# 停止一个已经在运行的容器
docker container stop 容器名或容器id
# 启动一个已经停止的容器
docker container start 容器名或容器id
# kill掉一个已经在运行的容器
docker container kill 容器名或容器id
- 删除容器
docker container rm 容器名或容器id
- 将容器保存为镜像
我们可以通过如下命令将容器保存为镜像
docker commit 容器名 镜像名
- 镜像备份与迁移
我们可以通过save命令将镜像打包成文件,拷贝给别人使用
- docker save -o 保存的文件名 镜像名
- docker save -o ./ubuntu.tar ubuntu
- 在拿到镜像文件后,可以通过load方法,将镜像加载到本地
- docker load -i ./ubuntu.tar
使用Docker安装FastDFS
-
获取镜像
- 获取镜像可以通过下载
docker image pull delron/fastdfs
- 也可以直接使用提供给大家的镜像备份文件
docker load -i 文件路径/fastdfs_docker.tar
- 加载好镜像后,就可以开启运行FastDFS的tracker和storage了
-
运行tracker
- 执行如下命令开启tracker 服务
# 我们将fastDFS tracker运行目录映射到本机的 /var/fdfs/tracker目录中
docker run -dti --network=host --name tracker -v /var/fdfs/tracker:/var/fdfs delron/fastdfs tracker
-
执行如下命令查看tracker是否运行起来
docker container ls
-
如果想停止tracker服务,可以执行如下命令
docker container stop tracker
-
停止后,重新运行tracker,可以执行如下命令
docker container start tracker
-
运行storage,执行如下命令开启storage服务
docker run -dti --network=host --name storage -e TRACKER_SERVER=10.211.55.5:22122 -v /var/fdfs/storage:/var/fdfs delron/fastdfs storage
- 注意事项
- TRACKER_SERVER=本机的ip地址:22122 本机ip地址不要使用127.0.0.1
- 我们将fastDFS storage运行目录映射到本机的/var/fdfs/storage目录中
-
执行如下命令查看storage是否运行起来
docker container ls
-
如果想停止storage服务,可以执行如下命令
docker container stop storage
-
停止后,重新运行storage,可以执行如下命令
docker container start storage
- 如果无法重新运行,可以删除/var/fdfs/storage/data目录下的fdfs_storaged.pid 文件,然后重新运行storage
安装FastDFS的Python客户端
pip install fdfs_client-py-master.zip # pip install py3Fdfs 也是可以的,而且几乎没坑,推荐这种方式安装(https://www.cnblogs.com/xcsg/p/11371091.html),用这种方式要参考网址来
pip install mutagen # 两个依赖库
pip isntall requests
- 安装报错踩坑记录
- 坑1: 报C++安装错误(装了,还是不能解决问题),解决办法
- 参考网址: https://blog.csdn.net/weixin_43796109/article/details/108194248
- 解压文件,执行 python setup.py install
- 进入 setup.py 文件,注释掉以下两行代码
sdict = {
......
'classifiers': [
'Development Status :: 1 - Production/Beta',
'Environment :: Console',
'Intended Audience :: Developers',
'License :: GPLV3',
'Operating System :: OS Independent',
'Programming Language :: Python'],
# 'ext_modules': [Extension('fdfs_client.sendfile',
# sources=['fdfs_client/sendfilemodule.c'])],
}
- 找到fdfs_client/storage_client.py文件打开注释下面的一行代码
# from fdfs_client.sendfile import *
- 一定要把 坑2 先改了,再执行 python setup.py install
- 会生成后缀为'xxx.egg'的文件,就对了
- 坑2:导入from fdfs_client.client import Fdfs_client报错: No module named 'mutagen._compat'
- 错误原因:导包时mutang._compat不存在,查看源码发现_compat在_senf文件夹下,
- 解决方式:需要将fdfs_client的utils.py的导包路径进行修改(使用压缩包zip去编辑保存即可)
# from mutagen._compat import StringIO
from mutagen._senf._compat import StringIO
- 坑3: client = Fdfs_client(conf_path='utils.fastdfs.client.conf')
报错: configparser.NoOptionError: No option 'connect_timeout' in section: '__config__'
- 解决办法:配置文件的路径,必须修改成绝对路径
client = Fdfs_client(conf_path=r'D:\Python\new_django\meiduo_mail\meiduo\meiduo\utils\fastdfs\client.conf')
-
项目utils目录新建'fastdfs包',把配置文件 client.conf复制进去
重要的配置就两个,其他默认即可- base_path(日志文件)
- tracker_server(服务器IP)
- 解压以后,python setup.py install 安装报错: #error: platfom not supported
### client.conf
# connect timeout in seconds
# default value is 30s
connect_timeout=30
# network timeout in seconds
# default value is 30s
network_timeout=120
# the base path to store log files
# base_path=/home/python/Desktop/python_Django/fdfs/log_files
# 配置fastdfs日志文件存放位置
base_path=/home/anning/Desktop/fastdfs/log_files
# tracker_server can ocur more than once, and tracker_server format is
# "host:port", host can be hostname or ip address
# 配置服务器IP地址
tracker_server=192.168.11.39:22122
#standard log level as syslog, case insensitive, value list:
### emerg for emergency
### alert
### crit for critical
### error
### warn for warning
### notice
### info
### debug
log_level=info
# if use connection pool
# default value is false
# since V4.05
use_connection_pool = false
# connections whose the idle time exceeds this time will be closed
# unit: second
# default value is 3600
# since V4.05
connection_pool_max_idle_time = 3600
# if load FastDFS parameters from tracker server
# since V4.05
# default value is false
load_fdfs_parameters_from_tracker=false
# if use storage ID instead of IP address
# same as tracker.conf
# valid only when load_fdfs_parameters_from_tracker is false
# default value is false
# since V4.05
use_storage_id = false
# specify storage ids filename, can use relative or absolute path
# same as tracker.conf
# valid only when load_fdfs_parameters_from_tracker is false
# since V4.05
storage_ids_filename = storage_ids.conf
#HTTP settings
http.tracker_server_port=80
#use "#include" directive to include HTTP other settiongs
##include http.conf
- fastdfs 客户端demo测试
- 测试demo之前,先把ubuntu虚拟机的tracker&storage服务开启
- docker container start tracker
- docker container start storage
from fdfs_client.client import Fdfs_client,get_tracker_conf
client = Fdfs_client(conf_path=r'D:\Python\new_django\meiduo_mail\meiduo\meiduo\utils\fastdfs\client.conf')
# res 返回值是一个字典,包裹反馈信息
res = client.upload_by_filename('demo.png')
'''
{'Group name': 'group1', 'Remote file_id': 'group1\\M00/00/00/wKgLJ2Oqat2AQsPXAAAMcih_5Rk631.png', 'Status': 'Upload successed.', 'Local file name': 'demo.png', 'Uploaded size': '3.00KB', 'Storage IP': '192.168.11.39'}
- 访问地址: http://192.168.11.39:8888/group1/M00/00/00/wKgLJ2Oqat2AQsPXAAAMcih_5Rk631.png
- 注意事项: 在ubuntu系统中,该图片有上锁标识,无法直接打开,但是拷贝到windows系统,就可以打开了
'''
- 如果出现远程服务器没有响应的问题,十有八九是远程服务器没有开放端口(图片访问的是8888端口也要开启)
- sudo ufw status # 查看端口开放情况
- sudo ufw all <端口号> # 允许某个端口
- sudo ufw enable # 开启防火墙
自定义Django文件存储系统
-
Django自带文件存储系统,但是默认文件存储在本地,在本项目中,我们需要将文件保存到FastDFS服务器上
所以需要自定义文件存储系统 -
存储类中必须实现_open()和_save()方法
以及任何后续使用中可能用到的其他方法(参照django文档) -
子类必须可以不用传任何参数(django规定)
from django.conf import settings
from django.core.files.storage import Storage
class FastDFSStorage(Storage):
def __init__(self):
pass
def _open(self,name,mode='rb'):
# 打开文件,而项目不需要打开,只上传而已,这里啥都不做
pass
def _save(self,name,content):
# 默认文件存储到本地,重写这个方法实现上传到fastdfs
# name是文件名,content是以'rb'模式打开的文件对象
# content.read()就是读取文件的二进制数据
# return file_id
pass
def exists(self, name):
pass
def url(self,name):
pass
from django.conf import settings
from django.core.files.storage import Storage
from fdfs_client.client import Fdfs_client
class FastDFSStorage(Storage):
def __init__(self):
pass
def _open(self,name,mode='rb'):
pass
def _save(self,name,content):
client = Fdfs_client(conf_path=r'D:\Python\new_django\meiduo_mail\meiduo\meiduo\utils\fastdfs\client.conf')
# upload_by_filename('xxx.png') 必须传入文件的绝对路径(上传的文件带有后缀),这里不满足需求
# 这个方法,可以传入文件的二进制数据(上传的文件不带后缀)
res = client.upload_by_buffer(content.read())
if res.get('Status') != 'Upload successed.':
raise Exception('Upload file failed')
file_id = res.get('Remote file_id')
return file_id
def exists(self, name):
'''
- 上传的时候,调用此方法判断文件是否已上传,如果没有上传则调用 save()方法进行上传
'''
# 标明文件需要上传,若返回True则表明文件不需要上传
return False
def url(self,name):
'''
- 要访问图片时,调用此方法获取图片文件的绝对路径
- name: 要访问的图片的 file_id
- return 完整图片的访问路径: storage_server IP:8888 + file_id
'''
return 'http://192.168.11.39:8888/' + name
- 最后,settings配置
#-----------文件存储配置-------#
DEFAULT_FILE_STORAGE = 'utils.fastdfs.fdfs_storage.FastDFSStorage'
富文本编辑器 CKEditor
- 参考网址
https://blog.csdn.net/qq_35709559/article/details/86544093
- 安装
pip install django-ckeditor
- 配置
### settings
......
NSTALLED_APPS = [
...
'ckeditor', # 富文本编辑器
'ckeditor_uploader', # 富文本编辑器上传图片模块
...
]
......
# 富文本编辑器ckeditor配置
CKEDITOR_CONFIGS = {
'default':{
'toolbar':'full', # 完整工具条
'height': 300, # 编辑高度
# 'woidth': 300, # 编辑宽度
},
}
CKEDITOR_UPLOAD_PATH = '' # 上传图片保存路径,使用了fastDFS,设置为''
### 总路由urls.py
urlpatterns = [
...
url(r'^ckeditor/', include('ckeditor_uploader.urls')),
]
-
为模型类添加字段
- ckeditor提供了两种类型的Django模型类字段
- ckeditor.fields.RichTextField 不支持上传文件的富文本字段
- ckeditor_uploader.fields.RichTextUploadingField 支持上传文件的富文本字段
# models.py
...
class Goods(BaseModel):
"""
商品SPU
"""
...
desc_detail = RichTextUploadingField(default='', verbose_name='详细介绍')
desc_pack = RichTextField(default='', verbose_name='包装信息')
desc_service = RichTextUploadingField(default='', verbose_name='售后服务')
...
- 添加FastDFS保存的测试图片数据
- 将/var/fdfs/storage中的data目录删除
- 将data.tar.gz文件拷贝到/var/fdfs/storage中,并解压缩
sudo cp -i data.tar.gz /local/arm # 拷贝
sudo tar -zxvf data.tar.gz # 解压
- 添加对应的数据库测试数据
mysql -h127.0.0.1 -uroot -pmysql meiduo_mall < goods_data.sql
删除了data目录以后,如果报以下错误,可以这么解决
- fdfs_client.exceptions.DataError: [-] Error: 2, No such file or directory
- 出错大致原因,data目录删除以后,原先storage ID就没了,此时依然向原来的 storage ID 传图片,显示是没有这个目录的
- 解决办法: 先让storage停止运行,然后删除storage
docker container stop storage
docker container rm storage
- 重新设置 storage
docker run -dti --network=host --name storage -e TRACKER_SERVER=192.168.11.39:22122 -v /var/fdfs/storage:/var/fdfs delron/fastdfs storage
- 再次启动storage,上传图片成功
docker container start storage
创建admin后台,测试效果
### contents.admin
......
admin.site.register(ContentCategory)
admin.site.register(Content)
### goods.admin
......
admin.site.register(models.GoodsCategory)
admin.site.register(models.GoodsChannel)
admin.site.register(models.Brand)
admin.site.register(models.Goods)
admin.site.register(models.GoodsSpecification)
admin.site.register(models.SpecificationOption)
admin.site.register(models.SKU)
admin.site.register(models.SKUImage)
admin.site.register(models.SKUSpecification)
使用"py3Fdfs"上传图片到"FastDFS"
- 参考网址
- https://www.cnblogs.com/xcsg/p/11371091.html
- https://www.cnblogs.com/jrri/p/11570089.html
- 安装依赖
#安装库
pip install py3Fdfs
pip install mutagen
pip isntall requests
- demo演示
from fdfs_client.client import Fdfs_client,get_tracker_conf
tracker_path = get_tracker_conf('client.conf')
client = Fdfs_client(tracker_path)
res = client.upload_by_filename('demo.png')
print(res)
'''
{'Group name': b'group1', 'Remote file_id': b'group1/M00/00/02/wKgLJ2Wo6MeAMMpBAAAMcih_5Rk860.png', 'Status': 'Upload successed.', 'Local file name': 'demo.png', 'Uploaded size': '3.11KB', 'Storage IP': b'192.168.11.39'}
'''
首页静态化
- 参考网址
https://blog.csdn.net/qq_35709559/article/details/86582028
- 业务场景: 首页的内容很多数据都要从db读取,如果每刷新一次就从db读取一次数据,当访问用户量大的时候,不仅服务器鸭梨大,db的开销也很大
- 解决办法: 缓存或者静态化
- 商场首页被频繁访问,为了提高访问速度,除了使用缓存技术外,还可以使用页面静态化技术
- 页面静态化即动态渲染生成的页面结果保存成html文件,放到静态服务器中
用户访问的时候访问的直接是处理好的html静态文件
对于页面中属于每个用户展示不同数据内容的部分, 可以在用户请求完静态化页面之后
再页面中向后端发起请求,获取数据属于用户的特殊的数据
- 好处显而易见,服务器/db的鸭梨小了很多
- 场景和思路分析
- 运维登录到admin后台,更新字段后,网页定时刷新内容(比如5分钟自动刷新一次)
- 静态化哪些数据
- 轮播图广告
- 商品分类
- 快讯活动广告(1Floor,2Floor,3Floor)
- 我们使用celery实现'定时任务'
celery 实现定时任务
- 参考网址(有坑,需要修改)
https://www.jianshu.com/p/22753ba20546
- 在配置文件中添加生成静态文件的保存路径
### settings
......
# 静态化主页存储路径
GENERATED_STATIC_HTML_FILES_DIR = os.path.join((os.path.dirname(BASE_DIR)), 'front_end')
- 在项目中新建templates模板目录,配置模板目录
......
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [os.path.join(BASE_DIR, 'templates')], # 新增
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],
},
},
]
...
- 在模板目录中新建index.html模板文件
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html;charset=UTF-8">
<title>美多商城-首页</title>
<link rel="stylesheet" type="text/css" href="http://127.0.0.1:8080/css/reset.css">
<link rel="stylesheet" type="text/css" href="http://127.0.0.1:8080/css/main.css">
<script type="text/javascript" src="http://127.0.0.1:8080/js/host.js"></script>
<script type="text/javascript" src="http://127.0.0.1:8080/js/vue-2.5.16.js"></script>
<script type="text/javascript" src="http://127.0.0.1:8080/js/axios-0.18.0.min.js"></script>
<script type="text/javascript" src="http://127.0.0.1:8080/js/jquery-1.12.4.min.js"></script>
</head>categories
<body>
<div id="app" v-cloak>
<div class="header_con">
<div class="header">
<div class="welcome fl">欢迎来到美多商城!</div>
<div class="fr">
<div v-if="username" class="login_btn fl">
欢迎您:<em>[[ username ]]</em>
<a @click="logoutfunc" class="quit">退出</a>
</div>
<div v-else class="login_btn fl">
<a href="/login.html">登录</a>
<span>|</span>
<a href="register.html">注册</a>
</div>
<div class="user_link fl">
<span>|</span>
<a href="/user_center_info.html">用户中心</a>
<span>|</span>
<a href="/cart.html">我的购物车</a>
<span>|</span>
<a href="/user_center_order.html">我的订单</a>
</div>
</div>
</div>
</div>
<div class="search_bar clearfix">
<a href="index.html" class="logo fl"><img src="../static/images/logo.png"></a>
<div class="search_wrap fl">
<form method="get" action="/search.html" class="search_con">
<input type="text" class="input_text fl" name="q" placeholder="搜索商品">
<input type="submit" class="input_btn fr" name="" value="搜索">
</form>
<ul class="search_suggest fl">
<li><a href="#">索尼微单</a></li>
<li><a href="#">优惠15元</a></li>
<li><a href="#">美妆个护</a></li>
<li><a href="#">买2免1</a></li>
</ul>
</div>
<div class="guest_cart fr">
<a href="/carts/" class="cart_name fl">我的购物车</a>
<div class="goods_count fl" id="show_count">[[ cart_total_count ]]</div>
<ul class="cart_goods_show">
<li v-for="cart in carts">
<img :src="cart.default_image_url" alt="商品图片">
<h4>[[ cart.name ]]</h4>
<div>[[ cart.count ]]</div>
</li>
</ul>
</div>
</div>
<div class="navbar_con">
<div class="navbar">
<h1 class="fl">商品分类</h1>
<ul class="navlist fl">
<li><a href="">首页</a></li>
<li class="interval">|</li>
<li><a href="">真划算</a></li>
<li class="interval">|</li>
<li><a href="">抽奖</a></li>
</ul>
</div>
</div>
<div class="pos_center_con clearfix">
{# 轮播图 广告 #}
<ul class="slide">
{% for lbt in contents.index_lbt %}
{# <li><a href="{{ lbt.url }}"><img src="http://image.meiduo.site:8888/{{ lbt.image }}" alt="{{ lbt.title }}"></a></li> #}
<li><a href="{{ lbt.url }}"><img src="{{ lbt.image.url }}" alt="{{ lbt.title }}"></a></li>
{% endfor %}
</ul>
<div class="prev"></div>
<div class="next"></div>
<ul class="points">
</ul>
{# 商品分类 #}
<ul class="sub_menu">
{% for group in categories.values %}
<li>
<div class="level1">
{% for chanel in group.channels %}
<a href="{{ chanel.url }}">{{ chanel.name }}</a>
{% endfor %}
</div>
<div class="level2">
{% for cat2 in group.sub_cats %}
<div class="list_group">
<div class="group_name fl">{{ cat2.name }}></div>
<div class="group_detail fl">
{% for cat3 in cat2.sub_cats %}
<a href="/list.html?cat={{ cat3.id }}">{{ cat3.name }}</a>
{% endfor %}
</div>
</div>
{% endfor %}
</div>
</li>
{% endfor %}
</ul>
<div class="news">
<div class="news_title">
<h3>快讯</h3>
<a href="#">更多 ></a>
</div>
<ul class="news_list">
{% for content in contents.index_kx %}
<li><a href="{{ content.url }}">{{ content.title }}</a></li>
{% endfor %}
</ul>
{% for content in contents.index_ytgg %}
<a href="{{ content.url }}" class="advs"><img src="{{ content.image.url }}"></a>
{% endfor %}
</div>
</div>
<div class="list_model">
<div class="list_title clearfix">
<h3 class="fl" id="model01">1F 手机通讯</h3>
<div class="subtitle fr">
<a @mouseenter="f1_tab=1" :class="f1_tab===1?'active':''">时尚新品</a>
<a @mouseenter="f1_tab=2" :class="f1_tab===2?'active':''">畅想低价</a>
<a @mouseenter="f1_tab=3" :class="f1_tab===3?'active':''">手机配件</a>
</div>
</div>
<div class="goods_con clearfix">
<div class="goods_banner fl">
<img src="{{ contents.index_1f_logo.0.image.url }}">
<div class="channel">
{% for content in contents.index_1f_pd %}
<a href="{{ content.url }}">{{ content.title }}</a>
{% endfor %}
</div>
<div class="key_words">
{% for content in contents.index_1f_bq %}
<a href="{{ content.url }}">{{ content.title }}</a>
{% endfor %}
</div>
</div>
<div class="goods_list_con">
<ul v-show="f1_tab===1" class="goods_list fl">
{% for content in contents.index_1f_ssxp %}
<li>
<a href="{{ content.url }}" class="goods_pic"><img src="{{ content.image.url }}"></a>
<h4><a href="{{ content.url }}" title="{{ content.title }}">{{ content.title }}</a></h4>
<div class="price">{{ content.text }}</div>
</li>
{% endfor %}
</ul>
<ul v-show="f1_tab===2" class="goods_list fl">
{% for content in contents.index_1f_cxdj %}
<li>
<a href="{{ content.url }}" class="goods_pic"><img src="{{ content.image.url }}"></a>
<h4><a href="{{ content.url }}" title="{{ content.title }}">{{ content.title }}</a></h4>
<div class="price">{{ content.text }}</div>
</li>
{% endfor %}
</ul>
<ul v-show="f1_tab===3" class="goods_list fl">
{% for content in contents.index_1f_sjpj %}
<li>
<a href="{{ content.url }}" class="goods_pic"><img src="{{ content.image.url }}"></a>
<h4><a href="{{ content.url }}" title="{{ content.title }}">{{ content.title }}</a></h4>
<div class="price">{{ content.text }}</div>
</li>
{% endfor %}
</ul>
</div>
</div>
</div>
<div class="list_model model02">
<div class="list_title clearfix">
<h3 class="fl" id="model01">2F 电脑数码</h3>
<div class="subtitle fr">
<a @mouseenter="f2_tab=1" :class="f2_tab===1?'active':''">加价换购</a>
<a @mouseenter="f2_tab=2" :class="f2_tab===2?'active':''">畅享低价</a>
</div>
</div>
<div class="goods_con clearfix">
<div class="goods_banner fl">
<img src="{{ contents.index_2f_logo.0.image.url }}">
<div class="channel">
{% for content in contents.index_2f_pd %}
<a href="{{ content.url }}">{{ content.title }}</a>
{% endfor %}
</div>
<div class="key_words">
{% for content in contents.index_2f_bq %}
<a href="{{ content.url }}">{{ content.title }}</a>
{% endfor %}
</div>
</div>
<div class="goods_list_con">
<ul v-show="f2_tab===1" class="goods_list fl">
{% for content in contents.index_2f_cxdj %}
<li>
<a href="{{ content.url }}" class="goods_pic"><img src="{{ content.image.url }}"></a>
<h4><a href="{{ content.url }}" title="{{ content.title }}">{{ content.title }}</a></h4>
<div class="price">{{ content.text }}</div>
</li>
{% endfor %}
</ul>
<ul v-show="f2_tab===2" class="goods_list fl">
{% for content in contents.index_2f_jjhg %}
<li>
<a href="{{ content.url }}" class="goods_pic"><img src="{{ content.image.url }}"></a>
<h4><a href="{{ content.url }}" title="{{ content.title }}">{{ content.title }}</a></h4>
<div class="price">{{ content.text }}</div>
</li>
{% endfor %}
</ul>
</div>
</div>
</div>
<div class="list_model model03">
<div class="list_title clearfix">
<h3 class="fl" id="model01">3F 家居家装</h3>
<div class="subtitle fr">
<a @mouseenter="f3_tab=1" :class="f3_tab===1?'active':''">生活用品</a>
<a @mouseenter="f3_tab=2" :class="f3_tab===2?'active':''">厨房用品</a>
</div>
</div>
<div class="goods_con clearfix">
<div class="goods_banner fl">
<img src="{{ contents.index_3f_logo.0.image.url }}">
<div class="channel">
{% for content in contents.index_3f_pd %}
<a href="{{ content.url }}">{{ content.title }}</a>
{% endfor %}
</div>
<div class="key_words">
{% for content in contents.index_3f_bq %}
<a href="{{ content.url }}">{{ content.title }}</a>
{% endfor %}
</div>
</div>
<div class="goods_list_con">
<ul v-show="f3_tab===1" class="goods_list fl">
{% for content in contents.index_3f_shyp %}
<li>
<a href="{{ content.url }}" class="goods_pic"><img src="{{ content.image.url }}"></a>
<h4><a href="{{ content.url }}" title="{{ content.title }}">{{ content.title }}</a></h4>
<div class="price">{{ content.text }}</div>
</li>
{% endfor %}
</ul>
<ul v-show="f3_tab===2" class="goods_list fl">
{% for content in contents.index_3f_cfyp %}
<li>
<a href="{{ content.url }}" class="goods_pic"><img src="{{ content.image.url }}"></a>
<h4><a href="{{ content.url }}" title="{{ content.title }}">{{ content.title }}</a></h4>
<div class="price">{{ content.text }}</div>
</li>
{% endfor %}
</ul>
</div>
</div>
</div>
<div class="footer">
<div class="foot_link">
<a href="#">关于我们</a>
<span>|</span>
<a href="#">联系我们</a>
<span>|</span>
<a href="#">招聘人才</a>
<span>|</span>
<a href="#">友情链接</a>
</div>
<p>CopyRight © 2016 北京美多商业股份有限公司 All Rights Reserved</p>
<p>电话:010-****888 京ICP备*******8号</p>
</div>
</div>
<script type="text/javascript" src="http://127.0.0.1:8080/js/slide.js"></script>
<script type="text/javascript" src="http://127.0.0.1:8080/js/common.js"></script>
<script type="text/javascript" src="http://127.0.0.1:8080/js/index.js"></script>
</body>
</html>
- 定时任务,需要用到 'django-crontab'插件(Linux系统才可以用,windows不能用)
- 而我们的开发环境是windows,所以,使用celery实现定时任务
- celery_tasks底下新建static_page包,底下新建 tasks.py
### tasks.py
from celery_tasks.main import celery_app
# demo: 测试定时任务是否配置成功
@celery_app.task(name='generate_index_page')
def generate_index_page():
print('实现定时刷新首页的功能')
'''
- 从db获取数据,然后渲染并生成模板,最后替换前端文件
'''
@celery_app.task(name='generate_static_index_html')
def generate_static_index_html():
"""
生成静态的主页html文件
"""
print('%s: generate_static_index_html' % time.ctime())
# 商品频道及分类菜单
# 使用有序字典保存类别的顺序
# categories = {
# 1: { # 组1
# 'channels': [{'id':, 'name':, 'url':},{}, {}...],
# 'sub_cats': [{'id':, 'name':, 'sub_cats':[{},{}]}, {}, {}, ..]
# },
# 2: { # 组2
#
# }
# }
categories = OrderedDict()
channels = GoodsChannel.objects.order_by('group_id', 'sequence')
for channel in channels:
group_id = channel.group_id # 当前组
if group_id not in categories:
categories[group_id] = {'channels': [], 'sub_cats': []}
cat1 = channel.category # 当前频道的类别
# 追加当前频道
categories[group_id]['channels'].append({
'id': cat1.id,
'name': cat1.name,
'url': channel.url
})
# 构建当前类别的子类别
for cat2 in cat1.goodscategory_set.all():
cat2.sub_cats = []
for cat3 in cat2.goodscategory_set.all():
cat2.sub_cats.append(cat3)
categories[group_id]['sub_cats'].append(cat2)
# 广告内容
contents = {}
content_categories = ContentCategory.objects.all()
for cat in content_categories:
contents[cat.key] = cat.content_set.filter(status=True).order_by('sequence')
# 渲染模板
context = {
'categories': categories,
'contents': contents
}
template = loader.get_template('index.html')
html_text = template.render(context)
file_path = os.path.join(settings.GENERATED_STATIC_HTML_FILES_DIR, 'index.html')
with open(file_path, 'w', encoding='utf-8') as f:
f.write(html_text)
### celery_tasks.config.py 新增配置
import os
from celery.schedules import crontab
from datetime import timedelta
# 指定任务队列存储的位置
broker_url = "redis://192.168.11.38:6379/7"
# 指定时区
timezone = 'Asia/Shanghai'
# 需要导入的任务模块(定时用)
imports = [
'celery_tasks.static_page.tasks'
]
# 需要执行任务的配置
beat_schedule = {
'static_page': { # 取个名称(我这里和目录名static_page保持一致)
# 具体需要执行的函数
# 该函数必须要使用@app.task装饰
# 注意目录的问题,由于上面已经有 imports配置路径,这里只需写函数名称即可()
# 若按照以下写法,会报模块路径错误,而导致定时任务被丢弃
# 'task': 'celery_tasks.static_page.tasks.generate_index_page',
'task': 'generate_index_page',
# 定时时间
# 每分钟执行一次,不能为小数
'schedule': crontab(minute='*/1'),
# 或者这么写,每小时执行一次
# "schedule": crontab(minute=0, hour="*/1")
# 执行的函数需要的参数
'args': ()
},
# 'test2': {
# 'task': 'celery_task.app_scripts.test2.test2_run',
# # 设置定时的时间,10秒一次
# 'schedule': timedelta(seconds=10),
# 'args': ()
# }
}
- 发布任务命令: celery -A celery_tasks.main beat # 在celery_task同级目录下
- 执行任务: celery -A celery_tasks.main worker -l info -P eventlet
- 线程报错问题
- Django3+celery4.4报错:
DatabaseWrapper objects created in a thread can only be used in that same thread
- 原因:
原因是eventlet对thread的获取线程id的方法get_ident()进行了重写,导致celery创造的线程id 和 原生的thread的get_ident()获取的id不一样
而django的db模块的代码,在数据库操作关闭时,会对创建这个连接进行验证是否是同 一个thread进行操作,如果不是一个操作,就会报错。验证是否为同一个id就是用原生的thread的get_ident()获取线程的id,导致报错
那解决方法就从双方获得的线程id需一致入手
原文链接:https://blog.csdn.net/lymmurrain/article/details/108668558
- 解决办法:
- 原先启动命令:
celery -A xxx worker -l info -P eventlet
- 替换为:
celery -A xxx worker -l info --pool=solo
商品列表页
-
参考网址:
https://blog.csdn.net/qq_35709559/article/details/86592903
-
当用户访问网址
/list.html?cat=xxx
时, 会进入商品列表页 -
cat参数
是用于过滤商品数据的第三季商品类别,也就是在商品列表中会根据cat参数
筛选商品数据用于展示
商品列表页逻辑分析
-
静态逻辑:商品分类数据展示(需要提前做静态化处理)
-
动态逻辑:商品面包屑导航数据和商品列表数据
-
由运维在
admin后台
编辑保存商品分类信息时,触发触发异步任务
,生成静态化页面
准备静态化异步任务并加入celery
### static_page.tasks
import os
import time
from collections import OrderedDict
from celery_tasks.main import celery_app
from apps.goods.models import GoodsChannel
from apps.contents.models import ContentCategory
from django.conf import settings
from django.template import loader
#---------商品列表页静态化--------------#
from apps.goods.models import GoodsChannel
def get_categories():
"""
获取商品分类菜单
:return:
"""
# 商品频道及分类菜单
# 使用有序字典保存类别的顺序
# categories = {
# 1: { # 组1
# 'channels': [{'id':, 'name':, 'url':},{}, {}...],
# 'sub_cats': [{'id':, 'name':, 'sub_cats':[{},{}]}, {}, {}, ..]
# },
# 2: { # 组2
#
# }
# }
categories = OrderedDict()
channels = GoodsChannel.objects.order_by('group_id', 'sequence')
for channel in channels:
group_id = channel.group_id # 当前组
if group_id not in categories:
categories[group_id] = {'channels': [], 'sub_cats': []}
cat1 = channel.category # 当前频道的类别
# 追加当前频道
categories[group_id]['channels'].append({
'id': cat1.id,
'name': cat1.name,
'url': channel.url
})
# 构建当前类别的子类别
for cat2 in cat1.goodscategory_set.all():
cat2.sub_cats = []
for cat3 in cat2.goodscategory_set.all():
cat2.sub_cats.append(cat3)
categories[group_id]['sub_cats'].append(cat2)
return categories
@celery_app.task(name='generate_static_list_search_html')
def generate_static_list_search_html():
"""
生成静态的商品列表页和搜索结果页html文件
:return:
"""
# 商品分类菜单
categories = get_categories()
# 渲染模板,生成静态文件
context = {
'categories':categories
}
template = loader.get_template('list.html') # templates.list.html
html_text = template.render(context)
file_path = os.path.join(settings.GENERATED_STATIC_HTML_FILES_DIR, 'list.html')
with open(file_path, 'w',encoding='utf-8') as f:
f.write(html_text)
运营人员站点中触发异步任务
# goods/admin.py(展示模型类数据,并触发异步任务)
from django.contrib import admin
# Register your models here.
from goods.models import GoodsCategory, Brand, Goods, SKU, GoodsChannel, GoodsSpecification, SpecificationOption, \
SKUSpecification, SKUImage
class GoodsCategoryAdmin(admin.ModelAdmin):
def save_model(self, request, obj, form, change):
obj.save() # 运维点击保存按钮
from celery_tasks.html.tasks import generate_static_list_search_html
generate_static_list_search_html.delay()
def delete_model(self, request, obj):
obj.delete() # 运维点击删除按钮
from celery_tasks.html.tasks import generate_static_list_search_html
generate_static_list_search_html.delay()
admin.site.register(GoodsCategory, GoodsCategoryAdmin)
admin.site.register(GoodsChannel)
admin.site.register(Goods)
admin.site.register(Brand)
admin.site.register(GoodsSpecification)
admin.site.register(SpecificationOption)
admin.site.register(SKU)
admin.site.register(SKUSpecification)
admin.site.register(SKUImage)
功能 --- 获取商品列表数据
-
当
list.html
加载完毕以后,根据list.html?cat=115
,立即向后端发起请求,获取商品列表数据 -
请求方式
请求方法 请求地址 GET http://127.0.0.1:8000/categories/(?P<category_id>\d+)/skus/?page= <; int:page>&page_size=int:page_size&ordering=str:ordering -
请求参数
参数名 类型 是否必传 说明 category_id int 是 商品分类ID(第三级) page int 是 请求页数 page_size int 否 每页数量 ordering str 是 排序关键字(‘create_time’, ‘price’, ‘sales’) -
响应成功结果:JSON
返回值 类型 是否必须 说明 count int 是 商品总数 next url 是 下一页链接地址 previous url 是 上一页链接地址 results sku[] 是 商品sku数据列表 id int 否 商品sku 编号 name str 否 商品名称 price decimal 否 单价 default_image_url str 否 默认图片 comments int 否 评论量 - 最关键的商品列表数据,保存在results { "count": 14, "next": "http://api.meiduo.site:8000/categories/115/skus/?page=2", "previous": null, "results": [ { "id": 3, "name": "Apple iPhone 8 Plus (A1864) 64GB 金色 移动联通电信4G手机", "price": "6499.00", "default_image_url": "http://image.meiduo.site:8888/group1/M00/00/02/CtM3BVrRZCqAUxp9AAFti6upbx41220032", "comments": 0 }, { "id": 4, "name": "Apple iPhone 8 Plus (A1864) 256GB 金色 移动联通电信4G手机", "price": "7988.00", "default_image_url": "http://image.meiduo.site:8888/group1/M00/00/02/CtM3BVrRZa6ANO_sAAFti6upbx40753757", "comments": 0 } ] }
后端实现
-
在
meiduo_mall/utils
中创建pagination.py
文件,并在其中创建分页配置类from rest_framework.pagination import PageNumberPagination class StandardResultsSetPagination(PageNumberPagination): page_size = 2 page_size_query_param = 'page_size' max_page_size = 20
-
在
settings
中配置REST framework
分页使用的分页类# settings.py ...... REST_FRAMEWORK = { # 分页 'DEFAULT_PAGINATION_CLASS': 'meiduo_mall.utils.pagination.StandardResultsSetPagination', }
-
定义序列化器(没有涉及跨表获取数据)
# ...apps/goods/serializers.py from rest_framework import serializers from goods.models import SKU class CategoriesListSerializer(serializers.ModelSerializer): """列表页序列化器""" # create_time = serializers.DateField(read_only=True) # update_time = serializers.DateField(read_only=True) class Meta: model = SKU fields = "__all__" # 根据实际需求,返回需要的字段(这里为了简单,全部返回)
-
在
goods/views.py
中实现视图from rest_framework.filters import OrderingFilter from rest_framework.generics import ListAPIView from .models import SKU from .serializers import CategoriesListSerializer class SKUListView(ListAPIView): """ sku列表数据 """ serializer_class = CategoriesListSerializer # 排序(分页的逻辑,在DRF全局配置中已完成) filter_backends = (OrderingFilter,) ordering_fields = ('create_time', 'price', 'sales') def get_queryset(self): """ 获取查询集 :return: """ return SKU.objects.filter(category_id=self.kwargs["pk"])
DRF提供了对于排序的支持,使用其提供的OrderingFilter过滤器后端即可
OrderingFilter过滤器要使用ordering_fields 属性来指明可以进行排序的字段有哪些
-
注册路由
# meiduo_mall/apps/goods/urls.py from django.conf.urls import url from goods import views urlpatterns = [ url(r"^categories/(?P<pk>\d+)/skus/$",views.SKUListView.as_view()), ]
-
前端代码
...... get_skus: function(){ axios.get(this.host+'/categories/'+this.cat+'/skus/', { params: { page: this.page, page_size: this.page_size, ordering: this.ordering }, responseType: 'json' }) .then(response => { this.count = response.data.count; this.skus = response.data.results; for(var i=0; i<this.skus.length; i++){ this.skus[i].url = '/goods/' + this.skus[i].id + ".html"; } }) .catch(error => { console.log(error.response.data); }) },
商品详情页(详情页面静态化)
-
商品详情页
依然采用静态化处理 -
商品详情页
的静态化由运维在编辑商品信息时触发生成静态化页面 -
套路和
商品列表页
类似
实现静态化异步任务
-
templates
新增detail.html
-
tasks.py
任务新增生成静态商品详情页面
逻辑...... @celery_app.task(name='generate_static_sku_detail_html') def generate_static_sku_detail_html(sku_id): """ 生成静态商品详情页面 :param sku_id: 商品sku_id :return: """ # 商品分类信息 categories = get_categories() # 获取当前sku的信息 sku = SKU.objects.filter(id=sku_id).first() sku.images = sku.skuimage_set.all() # 面包屑导航信息中的频道 goods = sku.goods goods.channel = goods.category1.goodschannel_set.all()[0] # 构建当前商品的规格键 # sku_key = [规格1参数id, 规格2参数id, 规格3参数id, ...] sku_specs = sku.skuspecification_set.order_by('spec_id') sku_key = [] for spec in sku_specs: sku_key.append(spec.option.id) # 构建当前商品的所有SKU skus = goods.sku_set.all() # 构建不同规格参数(选项)的sku字典 # spec_sku_map = { # (规格1参数id, 规格2参数id, 规格3参数id, ...): sku_id, # (规格1参数id, 规格2参数id, 规格3参数id, ...): sku_id, # ... # } spec_sku_map = {} for s in skus: # 获取sku的规格参数 s_specs = s.skuspecification_set.order_by('spec_id') # 用于形成规格参数-sku字典的key key = [] for spec in s_specs: key.append(spec) # 向规格参数-sku字典添加数据 spec_sku_map[tuple(key)] = s.id # 获取当前商品的规格信息 # specs = [ # { # 'name': '屏幕尺寸', # 'options': [ # {'value': '13.3寸', 'sku_id': xxx}, # {'value': '15.4寸', 'sku_id': xxx}, # ] # }, # { # 'name': '颜色', # 'options': [ # {'value': '银色', 'sku_id': xxx}, # {'value': '黑色', 'sku_id': xxx} # ] # }, # ... # ] specs = goods.goodsspecification_set.order_by("id") # 若当前sku的过个信息不完整,则不继续添加 if len(sku_key) < len(specs): return for index, spec in enumerate(specs): # 重复当前sku的规格键 key = sku_key[:] # 该规格的选项 options = spec.specificationoption_set.all() for option in options: # 在规格参数sku字典中查找符合当前规格的sku key[index] = option.id option.sku_id = spec_sku_map.get(tuple(key)) spec.options = options # 渲染模板,生成静态html文件 context = { 'categories': categories, 'goods': goods, 'specs': specs, 'sku': sku } template = loader.get_template('detail.html') html_text = template.render(context) file_path = os.path.join(settings.GENERATED_STATIC_HTML_FILES_DIR, 'goods/' + str(sku_id) + '.html') with open(file_path, 'w', encoding='utf-8') as f: f.write(html_text)
-
调整
Admin
站点保存和删除商品信息时行为- 在Admin站点保存或删除数据时,Django是调用的Admin站点管理器类的save_model()方法和delete_model() 方法,我们只需重新实现这两个方法,在这两个方法中调用异步任务即可
# ......apps/goods/admin.py from django.contrib import admin # Register your models here. from goods.models import GoodsCategory, Brand, Goods, SKU, GoodsChannel, GoodsSpecification, SpecificationOption, \ SKUSpecification, SKUImage class GoodsCategoryAdmin(admin.ModelAdmin): def save_model(self, request, obj, form, change): obj.save() from celery_tasks.html.tasks import generate_static_list_search_html generate_static_list_search_html.delay() def delete_model(self, request, obj): obj.delete() from celery_tasks.html.tasks import generate_static_list_search_html generate_static_list_search_html.delay() class SKUAdmin(admin.ModelAdmin): """SKU的管理""" def save_model(self, request, obj, form, change): """ 保存数据 :param request: :param obj: :param form: :param change: :return: """ obj.save() from celery_tasks.html.tasks import generate_static_sku_detail_html generate_static_sku_detail_html.delay(obj.id) class SKUSpecificationAdimn(admin.ModelAdmin): """sku规格""" def save_model(self, request, obj, form, change): obj.save() from celery_tasks.html.tasks import generate_static_sku_detail_html generate_static_sku_detail_html.delay(obj.sku.id) def delete_model(self, request, obj): sku_id = obj.sku.id obj.delete() from celery_tasks.html.tasks import generate_static_sku_detail_html generate_static_sku_detail_html.delay(sku_id) class SKUImageAdmin(admin.ModelAdmin): """图片管理""" def save_model(self, request, obj, form, change): obj.save() from celery_tasks.html.tasks import generate_static_sku_detail_html generate_static_sku_detail_html.delay(obj.sku.id) # 设置SKU默认图片 sku = obj.sku if not sku.default_image_url: sku.default_image_url = obj.image.url sku.save() def delete_model(self, request, obj): sku_id = obj.sku.id obj.delete() from celery_tasks.html.tasks import generate_static_sku_detail_html generate_static_sku_detail_html.delay(sku_id) admin.site.register(GoodsCategory, GoodsCategoryAdmin) admin.site.register(GoodsChannel) admin.site.register(Goods) admin.site.register(Brand) admin.site.register(GoodsSpecification) admin.site.register(SpecificationOption) admin.site.register(SKU, SKUAdmin) admin.site.register(SKUSpecification, SKUSpecificationAdimn) admin.site.register(SKUImage, SKUImageAdmin)
-
重启celery服务,测试效果
面包屑导航
- 作用: 告诉用户目前在网站中的位置以及如何返回
接口分析
-
请求方式
请求方法 请求地址 GET http://127.0.0.1:8000/categories/(?P<GoodsCategory_id>)/ -
请求参数
参数 类型 是否必传 说明 GoodsCategory_id int 是 三级分类id -
响应成功结果:JSON
参数 类型 是否必须 说明 cat1 str 否 一级分类名 cat2 str 否 二级分类名 cat3 str 否 三级分类名 -
实例
{ "cat1": 手机, "cat2": 手机通讯, "cat3": 手机 }
-
后端实现
# ...apps/goods/views.py ... class CategoriesView(APIView): """获取当前分类信息""" def get(self,request, pk): """ 1.获取前端数据 2. 查询当前三级分类信息 3.通过三级分类信息获取一二集分类 4. 返回 :param request: :return: """ cat3 = GoodsCategory.objects.get(id=pk) # 获取三级 cat2 = cat3.parent # 自关联获取二级, cat1 = cat2.parent # 自关联获取一级 # 返回数据 return Response({ "cat1": cat1.name, "cat2": cat2.name, "cat3": cat3.name })
-
添加路由
# .../apps/goods/urls.py ... urlpatterns = [ ... url(r"^categories/(?P<pk>\d+)/$", views.CategoriesView.as_view()), # 面包屑导航 ]
-
celery
命令小结
- 发布定时任务命令: celery -A celery_tasks.main beat # 在celery_task同级目录下
- 执行异步任务: celery -A celery_tasks.main worker -l info --pool=solo
使用 redis 存储商品的浏览记录
-
本质存储的是 商品ID
-
为什么不存在MySQL
- 因为这种数据没有必要作'持久化'
-
redis 实现流程
- 先去重(这里使用
redis-list类型
而不使用redis-set
,若使用set类型
,存在顺序问题) - 添加记录
- 最多存5条记录(切片)
- 先去重(这里使用
-
请求方式
请求方法 请求地址 POST http://127.0.0.1:8000/browse_histories -
请求参数
参数 类型 是否必传 说明 sku_id int 是 商品的sku_id -
响应成功结果:JSON
字段 类型 说明 sku_id int 商品的sku_id -
后端接口注意事项
- 首先,该接口必须带上jwt认证和权限 - 这是一个标准的新增db行为,所以继承 CreateAPIView 来写 - 序列化器 - 由于只有一个字段,简单继承 Serializer - 先校验 sku_id是否存在 - 增加create()方法,把字段存入redis,返回 validated_data
-
后端代码
### users.views
class UserBrowserHistoryView(CreateAPIView):
serializer_class = UserBrowserHistorySerializer
authentication_classes = [JSONWebTokenAuthentication, ]
permission_classes = [IsAuthenticated, ]
### users.serializers
# 只对sku_id进行校验
class UserBrowserHistorySerializer(serializers.Serializer):
sku_id = serializers.IntegerField(min_value=1, label='商品ID')
# 校验 sku_id是否存在
def validate_sku_id(self, value):
try:
SKU.objects.get(id=value)
except SKU.DoesNotExist:
raise serializers.ValidationError('sku_id不存在!')
return value
# 为每个用户在 redis中单独创建一个list,存储sku_id
def create(self, validated_data):
sku_id = validated_data.get('sku_id')
user = self.context['request'].user
conn = get_redis_connection('history')
pl = conn.pipeline()
user_sku_list = 'history_{}'.format(user.id)
pl.lrem(user_sku_list,0,sku_id) # 去重
pl.lpush(user_sku_list,sku_id) # 插入
pl.ltrim(user_sku_list,0,4) # 切片
pl.execute()
return validated_data
### users.urls
......
urlpatterns = [
......
# 商品浏览记录
url(r'^browse_histories/$', views.UserBrowserHistoryView.as_view()),
]
- 前端代码
......
// 页面加载完毕后,立即把 sku_id 发给后端
mounted: function(){
// 添加用户浏览历史记录
this.get_sku_id();
if (this.user_id) {
axios.post(this.host+'/browse_histories/', {
sku_id: this.sku_id
}, {
headers: {
'Authorization': 'JWT ' + this.token
}
})
}
this.get_cart();
this.get_hot_goods();
this.get_comments();
},
- 浏览商品记录,redis测试
- select 3
- keys * # 啥都没
- keys * # 点击商品浏览记录
- "history_1"
- lrange history_1 0 -1
- "16"
商品浏览记录的展示
-
是否可以用
ListAPIView
- 不可以,使用
ListAPIView
,涉及到queryset
,涉及到模型,而我们是把数据存入redis
,没有模型这种概念
- 不可以,使用
-
为了节省路由,把查询商品记录的逻辑,写在
UserBrowserHistoryView
,实现get
方法即可 -
思路:获取redis数据中SKU商品的ids集合,去mysql中查询对应的SKU记录,序列化返回给前端显示
### views
......
class UserBrowserHistoryView(CreateAPIView):
serializer_class = UserBrowserHistorySerializer
authentication_classes = [JSONWebTokenAuthentication, ]
permission_classes = [IsAuthenticated, ]
# 序列化历史商品记录
def get(self,request):
conn = get_redis_connection('history')
user = request.user
user_sku_list = 'history_{}'.format(user.id)
sku_ids = conn.lrange(user_sku_list,0,-1) # <class 'list'> [b'1', b'16', b'12', b'15', b'2']
# 以下写法,顺序有可能会乱掉,所以不这么写
# sku_ids_list = SKU.objects.filter(id__in=sku_ids)
sku_ids_list = []
for id in sku_ids:
obj = SKU.objects.get(id=id)
sku_ids_list.append(obj)
serializer = SKUSerializer(sku_ids_list,many=True)
return Response(serializer.data)
### serializers
......
class SKUSerializer(serializers.ModelSerializer):
class Meta:
model = SKU
fields = ('id','name','price','default_image_url','comments')
- 前端代码
......
mounted: function () {
// 判断用户的登录状态
......
// 补充请求浏览历史
axios.get(this.host + '/browse_histories/', {
headers: {
'Authorization': 'JWT ' + this.token
},
responseType: 'json'
})
.then(response => {
// 渲染数据
this.histories = response.data;
for (var i = 0; i < this.histories.length; i++) {
this.histories[i].url = '/goods/' + this.histories[i].id + '.html';
}
})
})
.catch(error => {
if (error.response.status == 401 || error.response.status == 403) {
location.href = '/login.html?next=/user_center_info.html';
}
});
......
},
- 也可以把
views
逻辑修改成ListAPIView
,但是代码实际没精简多少(还是推荐上面的写法)
class UserBrowserHistoryListView(ListAPIView):
serializer_class = SKUSerializer
authentication_classes = [JSONWebTokenAuthentication, ]
permission_classes = [IsAuthenticated, ]
def get_queryset(self):
conn = get_redis_connection('history')
user = self.request.user
user_sku_list = 'history_{}'.format(user.id)
sku_ids = conn.lrange(user_sku_list, 0, -1)
sku_ids_list = []
for id in sku_ids:
obj = SKU.objects.get(id=id)
sku_ids_list.append(obj)
return sku_ids_list # 返回list
'''
{
"count": 5,
"next": "http://127.0.0.1:8000/browse_histories/?page=2",
"previous": null,
"results": [
{
"id": 13,
"name": "华为 HUAWEI P10 Plus 6GB+64GB 玫瑰金 移动联通电信4G手机 双卡双待",
"price": "3388.00",
"default_image_url": "http://192.168.11.39:8888/group1/M00/00/02/CtM3BVrRdLGARgBAAAVslh9vkK00474545",
"comments": 0
},
{
"id": 12,
"name": "华为 HUAWEI P10 Plus 6GB+64GB 钻雕蓝 移动联通电信4G手机 双卡双待",
"price": "3388.00",
"default_image_url": "http://192.168.11.39:8888/group1/M00/00/02/CtM3BVrRdICAO_CRAAcPaeOqMpA2024091",
"comments": 0
}
]
}
'''