电商概念: 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 }}&gt;</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="#">更多 &gt;</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
		}
	]
}
'''