电商概念: 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):
    """
    商品类别(分组)
    """
    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):
    """
    商品频道
    """
    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):
    """
    品牌
    """
    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
    """
    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):
    """
    商品规格
    """
    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):
    """
    规格选项
    """
    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
    """
    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图片
    """
    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具体规格
    """
    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):
    """
    广告内容类别(分组)
    """
    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):
    """
    广告内容(主表)
    """
    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

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 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
'''

  • 如果出现远程服务器没有响应的问题,十有八九是远程服务器没有开放端口(图片访问的是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

页面静态化

  • 参考网址

https://blog.csdn.net/qq_35709559/article/details/86582028

  • 商场首页被频繁访问,为了提高访问速度,除了使用缓存技术外,还可以使用页面静态化技术
页面静态化即动态渲染生成的页面结果保存成html文件,放到静态服务器中
用户访问的时候访问的直接是处理好的html静态文件
对于页面中属于每个用户展示不同数据内容的部分, 可以在用户请求完静态化页面之后
再页面中向后端发起请求,获取数据属于用户的特殊的数据
### contents.crons.py
'''
- 从db获取数据,然后渲染模板,最后放到前端文件
'''
from collections import OrderedDict
from django.conf import settings
from django.template import loader
import os
import time

from goods.models import GoodsChannel
from .models import ContentCategory


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)

  • 在配置文件中添加生成静态文件的保存路径
### 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模板文件

  • 定时任务,需要用到 'django-crontab'插件(Linux系统才可以用,windows不能用)

使用 redis 存储商品的浏览记录

  • 本质存储的是 商品ID

  • 为什么不存在MySQL

    • 因为这种数据没有必要作'持久化'
  • redis 实现流程

    • 先去重

    • 添加记录

    • 最多存5条记录(切片)

  • 后端代码

### views
# 增加一条记录,使用 CreateAPIView
class UserBrowserHistoryView(CreateAPIView):

    serializer_class = UserBrowserHistorySerializer # 自定义序列化器
    permission_classes = [IsAuthenticated] # 校验不能丢

### 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_list_name = 'history_{}'.format(user.id)
        pl.lrem(user_list_name, 0, sku_id)
        pl.lpush(user_list_name, sku_id)
        pl.ltrim(user_list_name, 0, 4)
        pl.execute()

        return validated_data

### 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,没有模型这种概念
  • 为了节省路由,把查询商品记录的逻辑,写在之前的View

### views
......
class UserBrowserHistoryView(CreateAPIView):

    serializer_class = UserBrowserHistorySerializer
    permission_classes = [IsAuthenticated]

    # 展示商品记录
    def get(self,request):

        conn = get_redis_connection('history')
        user = request.user
        user_list_name = 'history_{}'.format(user.id)
        sku_ids = conn.lrange(user_list_name,0,-1)

        # 以下写法,顺序有可能会乱掉,所以不这么写
        # sku_list = SKU.objects.filter(id__in=sku_ids)
        sku_list = []

        for sku_id in sku_ids:
            obj = SKU.objects.get(id=sku_id)
            sku_list.append(obj)
        
        # 自定义序列化器
        serializer = SKUSerializer(sku_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';
                }
            });
    ......
},