商品开始前数据库分析与需要的技术
数据库分析:https://www.cnblogs.com/kuxingseng95/articles/9294979.html
创建商品应用和广告应用:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
python ../../manage.py startapp contents
python ../../manage.py startapp goods
将创建的注册到settings中的app管理中。
修改商品表goods的models.py
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
from django.db import models from meiduo_mall.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.py
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
from django.db import models from meiduo_mall.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
然后执行迁移
数据库问题解决了,下面要考虑图片的存储了,在django中,我们可以通过admin将图片保存到本地,但是,本地无法解决图片的存储问题。
比如:上传了两个同名但是不同的文件和上传了不同名但是相同的文件。
我们使用像七牛云这样的存储图片的服务就是用来解决这些问题的。图片存储一般都是用二进制方式,这些提供图片存储的服务的一般是计算文件指纹的方法来实现存储,不依靠用户提供的名字进行判断,是依靠图片本身。他们用根据存储图片的二进制代码通过md5或者sha1或者sha256这些加密方式来进行计算获得一个值。这些计算的好处就是同一个数据传进去算出来的是一样的参数,而不同的数据,哪怕差的只是一个字符,都会产生不同的数据。这样计算出来的值就叫做文件的指纹了,是这个文件的唯一标识。而在真正存储的时候是用这个指纹。除了这两个问题,还有一个问题就是拓展问题,当你的硬盘存满了, 你会想到换个大的。但是换的时候,我们需要将项目停止。所以我们想到要找个机器在原有项目不停机的情况下,稍微配置一下,可以和原有数据组合,还有就是数据的备份问题。所以我们花钱买方便,去让七牛云这样的平台去给我们提供解决方案。
当公司规模比较大,租用不如自己搭建合适的时候,就会考虑到自己去设计解决的方案了。这时候就会使用FastDFS的解决方案了。FastDFS是淘宝在设计电商图片存储时使用的技术,后来给开源出来。
FastDFS:https://www.cnblogs.com/kuxingseng95/articles/9296022.html
Docker:https://www.cnblogs.com/kuxingseng95/p/9296284.html
FastDFS客户端与自定义文件存储系统
FastDFS的Python客户端
python版本的FastDFS客户端使用说明参考https://github.com/jefforeilly/fdfs_client-py
安装
安装提供给大家的fdfs_client-py-master.zip到虚拟环境中
pip install fdfs_client-py-master.zip
pip install mutagen
pip isntall requests
在实际使用的时候,我首先将fdfs_client-py-master放到项目路径下的scripts中,
然后终端进入scrips中,使用命令:
pip install ./fdfs_client-py-master.zip
进行安装
然后安装依赖包,这个就可以用网络的方式来了
pip install mutagen
pip install requests
使用
使用的时候我们需要创建一个对象。但是在这个之前,我们还要进行配置,因为根据FastDFS的流程,client要跟tracker进行沟通,而tracker server在哪?不能写死,所以要在配置文件中写出来。
而这个配置文件也是FastDFS提供的。我们只要修改里面的某些参数就可以了。
在和项目同名的包meiduo_mall中的utils中加入一个配置文件,client.conf。
然后修改两个地方
一个应该是第十行的:base_path,这个是用来配置存放日志文件的地方。可以放到你像放到的地方,项目中的logs或者其他地方都可以。
还有一个应该是第十四行的tracker_server,这个用来指定tracker_server的地址。这个ip地址和启动storage的地方是一样的。
base_path=FastDFS客户端存放日志文件的目录 tracker_server=运行tracker服务的机器ip:22122
修改了之后测试一下:
python manage.py shell进入django的shell中
>>> from fdfs_client.client import Fdfs_client >>> client = Fdfs_client('meiduo_mall/utils/fastdfs/client.conf') >>> ret = client.upload_by_filename('/home/python/Desktop/123.PNG') getting connection <fdfs_client.connection.Connection object at 0x7fdafb285cf8> <fdfs_client.fdfs_protol.Tracker_header object at 0x7fdafb285f60> >>> ret {'Storage IP': '192.168.233.138', 'Local file name': '/home/python/Desktop/123.PNG', 'Group name': 'group1', 'Uploaded size': '238.00KB', 'Status': 'Upload successed.', 'Remote file_id': 'group1/M00/00/00/wKjpiltG59yAWvXYAAO6y4bvfos602.PNG'} >>>
有类似这样的返回值就说明妥了。
在镜像源中的Storager已经安装好了NGINX了,端口是8888。我们要看到要上传的图片的话,只要浏览器访问NGINX所在ip地址(storager所在的ip地址)加8888端口号加上面ret参数中的唯一指纹Remote file_id就可以了。
我们打算用FastDFS来存储图片,而在django中有默认的文件存储,当我们给一个模型设置为ImageField类型的时候,可以通过django的默认的文件存储方式存储这个图片,他是保存到服务中了,我们需要的就是修改他的文件存储系统,让它使用我们的FastDFS。
按照惯例分析一波:
- 因为后续的商品和广告这两个应用中都要用这个文件存储系统,所以我将整个配置放到了和项目同名的utils文件中。在里面我新建了fdfs_storage.py。
- 要明确在django中,它的默认文件存储系统设置放到了django.core.files.storage.Storage中,所以我们需要继承并重写里面的方法。里面有很多的方法,但是如果要修改默认的文件存储的话必须实现_open()和_save()方法,以及以后任何后续使用中可能用到的其他方法。
- 明确了必须重写_open()和_save()方法以后。
- 我们要往FastDFS中传文件的话,是不用文件系统帮我们打开的,我们只需要它把内容给我发到FastDFS中去就可以了,所以_open()方法中直接用pass就行了。
- 保存的大致流程在之前用django的shell的时候我们就已经演示过了,现在就是对细节的说明
- 首先,关于配置信息,我们要明确的就是不应该写死到这个里面,以后不好复用,应该也是将主要的参数写到配置信息,也就是settings中
- 关于参数,django在创建我们自定义的文件存储的时候,它可能不会去给我们传参数,所以,要把参数放到构造中,就必须加为None的默认。然后再后面给他写好如果不传参数用settings文件中的配置信息。
- _save方法django在调用的时候name是前端传过来的名字,content就是django从请求体中取出来的当作文件对象传过来的。文件对象的读取方法就是以前学的文件操作read()方法。由于我们获得的不是整个的文件,所以用的是
upload_by_buffer
()这个二进制的上传方式 - 上传之后会有返回值,返回值是一个字典,字典中有一个键是Status,用来记录保存的状态,
"Upload successed."
表示成功。成功的话,我们就可以把唯一的图片指纹Remote file_id
拿到了。 - 处理完了之后怎么保存呢?django中在自动调用_save()方法后,你返回什么它就存什么。
- 除了_open()和_save()方法,还有一些其他的方法,并不是这些方法全部都要实现,可以省略用不到的方法。
- exists(name)
- 如果名为name的文件在文件系统中存在,则返回True,否则返回False。
- 这个实际上我们也必须要重写,因为django在调用_save方法前会先调用这个方法,看这个名字的文件有没有,如果有的话就不会调用_save方法。
- 这里我们不管传什么,我们都返回False,因为FastDFS能帮我们处理了。
- url(name)
- 返回文件的完整访问URL
- 如果是以前默认的django文件系统中,我们可以用,比如:image.url来获取这个文件的完整路径。它调用的就是这个url方法。它就会把当前对象数据库中保存的那个模型类的对应字段的值拿出来作为name参数,然后再返回一个值。
- 在前面返回的时候我们只返回了一个文件的指纹,事实上根据前面的例子,我们可以看出来,要获取完整的路径,我们还缺少了前部分。所以我们可以从配置参数中设置一个值,然后拼接作为值返回。
- delete(name)
- 删除name的文件
- listdir(path)
- 返回name文件的总大小
- exists(name)
- 还有一个最重要的就是要给这个自定义的文件存储中盖个章(加上装饰器)@deconstructible,它继承自from django.utils.deconstruct import deconstructible
在和项目同名的文件夹meiduo_mall中的utils中的fastdfs中新建fdfs_storage.py
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
from django.conf import settings from django.core.files.storage import Storage from django.utils.deconstruct import deconstructible from fdfs_client.client import Fdfs_client @deconstructible class FastDFSStorage(Storage): def __init__(self, base_url=None, client_conf=None): """ 初始化 :param base_url: 用于构造图片完整路径使用,图片服务器的域名 :param client_conf: FastDFS客户端配置文件的路径 """ if base_url is None: base_url = settings.FDFS_URL self.base_url = base_url if client_conf is None: client_conf = settings.FDFS_CLIENT_CONF self.client_conf = client_conf def _open(self, name, mode='rb'): pass def _save(self, name, content): """ 在FastDFS中保存文件 :param name: 传入的文件名 :param content: 文件对象 :return: 保存到数据库中的FastDFS的文件名 """ client = Fdfs_client(self.client_conf) ret = client.upload_by_buffer(content.read()) if ret.get("Status") != "Upload successed.": raise Exception("upload file failed") file_name = ret.get("Remote file_id") return file_name def exists(self, name): return False def url(self, name): return self.base_url + name
在settings/dev.py文件中添加设置
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
# django文件存储 DEFAULT_FILE_STORAGE = 'meiduo_mall.utils.fastdfs.fdfs_storage.FastDFSStorage' # FastDFS FDFS_URL = 'http://image.meiduo.site:8888/' FDFS_CLIENT_CONF = os.path.join(BASE_DIR, 'utils/fastdfs/client.conf')
注:第一个配置是告诉django用我们自己定义的文件存储类,第二个配置也不用ip地址写死了,使用域名。
在/etc/hosts中添加访问FastDFS storage服务器的域名
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
127.0.0.1 image.meiduo.site
CKEditor富文本编辑器
在运营后台,运营人员需要录入商品并编辑商品的详情信息,而商品的详情信息不是普通的文本,可以是包含了HTML语法格式的字符串。为了快速简单的让用户能够在页面中编辑带格式的文本,我们引入富文本编辑器。富文本即具备丰富样式格式的文本。
我们使用功能强大的CKEditor富文本编辑器。
安装
pip install django-ckeditor
添加应用
在INSTALLED_APPS中添加
在这个CKEditor富文本编辑器中,有一种简单模式,就是不加图片的,一种是加图片的,而我们需要加图片,所以,都用了。
INSTALLED_APPS = [ ... 'ckeditor', # 富文本编辑器 'ckeditor_uploader', # 富文本编辑器上传图片模块 ... ]
添加CKEditor设置
在settings/dev.py中添加 # 富文本编辑器ckeditor配置 CKEDITOR_CONFIGS = { 'default': { 'toolbar': 'full', # 工具条功能,这个表示所有的工具全都展示出来 'height': 300, # 编辑器高度 # 'width': 300, # 编辑器宽 }, } CKEDITOR_UPLOAD_PATH = '' # 上传图片保存路径,使用了FastDFS,所以此处设为'',如果不设置的话,它就会发现缺少参数,然后报错。
添加ckeditor路由
在总路由中添加
url(r'^ckeditor/', include('ckeditor_uploader.urls')),
为模型类添加字段
ckeditor提供了两种类型的Django模型类字段
ckeditor.fields.RichTextField
不支持上传文件的富文本字段ckeditor_uploader.fields.RichTextUploadingField
支持上传文件的富文本字段
在商品模型类(SPU)中,要保存商品的详细介绍、包装信息、售后服务,这三个字段需要作为富文本字段,这样写了之后,它就和数据库集中到一起了。你哪个字段要进行编辑,它就会用富文本编辑器的方式来让你处理。
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
from ckeditor.fields import RichTextField from ckeditor_uploader.fields import RichTextUploadingField class Goods(BaseModel): """ 商品SPU """ ... desc_detail = RichTextUploadingField(default='', verbose_name='详细介绍') desc_pack = RichTextField(default='', verbose_name='包装信息') desc_service = RichTextUploadingField(default='', verbose_name='售后服务')
将需要的表注册到admin中
apps/contents/admin.py
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
from django.contrib import admin from . import models # Register your models here. admin.site.register(models.ContentCategory) admin.site.register(models.Content)
apps/goods/admin.py
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
from django.contrib import admin from . import models # Register your models here. admin.site.register(models.GoodsCategory) admin.site.register(models.GoodsChannel) admin.site.register(models.Goods) admin.site.register(models.Brand) admin.site.register(models.GoodsSpecification) admin.site.register(models.SpecificationOption) admin.site.register(models.SKU) admin.site.register(models.SKUSpecification) admin.site.register(models.SKUImage)
调试
创建admin管理员
python manage.py createsuperuser
进入admin的商品中,我们上传图片后,会报一个错误,
源码就是这里
出错的原因就是ckeditor在处理上传后的文件名按照有后缀名来处理,而我们存到数据库中的是'group1/M00/00/00/wKjpiltG59yAWvXYAAO6y4bvfos602.PNG'这样的数据,ckeditor在处理的时候拿点来进行分隔,而我们的数据没有点,那它用下标取第二个数的时候就会产生下标越界了。
可以看到它源码中的写的太死了,所以我们需要对它进行修改,源码的路径是
可以等出错了从错误信息中点,也可以到路径下去找,如果觉得找的麻烦的话,可以到models中ctrl+鼠标点RichTextUploadingField,找到倒数第二个包之后,点一下这个包,就可以选择这个包里面的views.py中了。
修改之后的(就是加了一个if判断,如果带点就再判断,不带点就直接执行下面的命令)
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
if len(str(saved_path).split('.'))>1: if(str(saved_path).split('.')[1].lower() != 'gif'): self._create_thumbnail_if_needed(backend, saved_path) url = utils.get_media_url(saved_path)
放入测试数据
数据:https://pan.baidu.com/s/1Uxsp8ja6qolm_4pi4HrAaA
数据库数据加入:进入到数据存放的目录中,终端执行
mysql -uroot -pmysql meiduo < ./goods_data.sql
图片数据的加入,进入到storage中写好的宿主映射的路径,我设置的路径是:/var/fdfs/storage,进入存放数据目录
tar -zxvf data.tar.gz
sudo mv data /var/fdfs/storage/
页面静态化
商城的首页频繁被访问,为了提升访问速度,除了我们之前已经学过的使用缓存技术外,还可以使用页面静态化技术。
页面静态化即将动态渲染生成的页面结果保存成html文件,放到静态文件服务器中。用户访问的时候访问的直接是处理好之后的html静态文件。
对于页面中属于每个用户展示不同数据内容的部分,可以在用户请求完静态化之后的页面后,在页面中向后端发送请求,获取属于用户的特殊的数据。
页面静态话其实主要使用的是django的模板,views中的render进行渲染还有文件的读写操作。
完成静态化应该是分为三步:
- 在配置信息中告诉django哪个地方的模板需要渲染。
- 定义模板
- 模板渲染
关于模板
需要注意的是django的模板语法
在进行模板渲染的时候,有两种方式:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
# 第一种: from django.template import loader # 导入loader模块,loader又被称为加载器 def index(request): template = loader.get_template # 使用loader中的get_template("模板的名字")方法,它就会把模板的内容读出来了。现在还没有开始进行渲染,现在template是一个对象了。 context={'city': '北京'} # 构造上下文 return HttpResponse(template.render(context)) # 将参数传到这个模板对象template中,然后它会自己去渲染。 # template.render()这个方法会返回的实际是一个文本的数据。 # 可以理解为是一个字符串。就是把模板中需要填充的数据都填充好了之后的文本数据。 # HttpResponse就是response的一个子类,我们的文本是没有办法传输的,所以要把这个生成的文本内容嵌入到响应对象中去。 #****************************************************** # 第二种: from django.shortcuts import render def index(request): context={'city': '北京'} # 构造上下文 return render(request, 'index.html, context) # 这是简写形式,而这个返回结果和第一种的返回结果是一样的。
那么这两种方式我们用页面静态化的时候使用哪种呢?
第二种返回的是一个响应的对象,而我们需要的不是一个响应对象,而是一个渲染好的存储在后端的静态页面,所以我们用的是第一种渲染方式。
首页页面静态化前配置
1.修改配置信息
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
2.在广告内容应用contents中,新建crons.py文件(该文件会用于后面讲解的定时任务),在该文件中编写处理页面静态化的逻辑。
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
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)
3.在meiduo_mall 中新建templates模板目录,配置模板目录,在模板目录中新建index.html模板文件
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
<!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="css/reset.css"> <link rel="stylesheet" type="text/css" href="css/main.css"> <script type="text/javascript" src="js/host.js"></script> <script type="text/javascript" src="js/vue-2.5.16.js"></script> <script type="text/javascript" src="js/axios-0.18.0.min.js"></script> <script type="text/javascript" src="js/jquery-1.12.4.min.js"></script> <script type="text/javascript" src="js/slide.js"></script> </head> <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> <span>|</span> <a @click="logout">退出</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="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="#" class="cart_name fl">我的购物车</a> <div class="goods_count fl" id="show_count">15</div> <ul class="cart_goods_show"> <li> <img src="images/goods/goods001.jpg" alt="商品图片"> <h4>商品名称手机</h4> <div>4</div> </li> <li> <img src="images/goods/goods002.jpg" alt="商品图片"> <h4>商品名称手机</h4> <div>5</div> </li> <li> <img src="images/goods/goods003.jpg" alt="商品图片"> <h4>商品名称手机</h4> <div>6</div> </li> <li> <img src="images/goods/goods003.jpg" alt="商品图片"> <h4>商品名称手机</h4> <div>6</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 content in contents.index_lbt %} <li><a href="{{ content.url }}"><img src="{{ content.image.url }}" alt="{{ content.title }}"></a></li> {% endfor %} </ul> <div class="prev"></div> <div class="next"></div> <ul class="points"> <!-- <li class="active"></li> <li></li> <li></li> <li></li> --> </ul> <ul class="sub_menu"> {% for group in categories.values %} <li> <div class="level1"> {% for channel in group.channels %} <a href="{{ channel.url }}">{{ channel.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> <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="prize">{{ 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="prize">{{ 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="prize">{{ content.text }}</div> </li> {% endfor %} </ul> </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> <ul v-show="f2_tab===1" 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="prize">{{ content.text }}</div> </li> {% endfor %} </ul> <ul v-show="f2_tab===2" 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="prize">{{ content.text }}</div> </li> {% endfor %} </ul> </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> <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="prize">{{ 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="prize">{{ content.text }}</div> </li> {% endfor %} </ul> </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="js/index.js"></script> </body> </html>
4.在前端js目录中新建index.js文件
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
var vm = new Vue({ el: '#app', // 声明Vue使用的模板变量语法 delimiters: ['[[', ']]'], data: { host, username: sessionStorage.username || localStorage.username, user_id: sessionStorage.user_id || localStorage.user_id, token: sessionStorage.token || localStorage.token, cart_total_count: 0, // 购物车总数量 cart: [], // 购物车数据, f1_tab: 1, // 1F 标签页控制 f2_tab: 1, // 2F 标签页控制 f3_tab: 1, // 3F 标签页控制 }, mounted: function(){ this.get_cart(); }, methods: { // 退出 logout: function(){ sessionStorage.clear(); localStorage.clear(); location.href = '/login.html'; }, // 获取购物车数据 get_cart: function(){ } } });
关于静态化
在页面静态化的时候有主要分为了两种,定时任务,就是每隔多少时间,生成一个静态化的页面,首页一般是这样做的。还有就是对于长时间不变的页面,我们只需要在它改变的时候生成静态化页面就可以了,这个主要是用在商品详情页。而在详情页中,有些地方是需要实时刷新的,比如商品详情中的热销商品和商品评价,所以页面静态化并不是页面完全写死,而是你需要哪些静态化就让它静态化,哪些需要获取新的数据,就去动态请求。。
定时任务及首页静态化实现
在Django执行定时任务,可以通过django-crontab扩展来实现。
安装
pip install django-crontab
添加应用
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
INSTALLED_APPS = [ ... 'django_crontab', # 定时任务 ... ]
设置任务的定时时间
在配置文件dev.py中的配置如下
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
# 定时任务 CRONJOBS = [ # 每5分钟执行一次生成主页静态文件 # ('*/5 * * * *', 'contents.crons.generate_static_index_html', '>> /home/python/Desktop/meiduo/django_meiduo/meiduo_mall/logs/crontab.log') ('*/1 * * * *', 'contents.crons.generate_static_index_html', '>> ' + os.path.join(os.path.dirname(BASE_DIR), "logs/crontab.log")) ]
注:
在CRONJOBS的列表中,可以包含多个定时任务,每个任务用元组来进行设置。设置的时候分为三个部分:
第一部分:任务时间 要用字符串来传,代表着间隔信息,或者说执行的时间点。 基本格式 "*****"五个星号分别对应"分时日月周" 比如第一个设置分的,你可以指定为*,代表不做具体设置,也可以指定为0-59之间的数字,这个不是设置执行的时间间隔,而是每小时的第几分钟执行,
比如设置为10,表示0:10,1:10这样的时间点刷新。同理给分设置为10,给时设置为0,则表示每天的0点10分执行。 所以在设置的时候,设置时间的区间是: M: 分钟(0-59);H:小时(0-23);D:天(1-31);m: 月(1-12);d: 一星期内的天(0~6,0为星期天) 那么如果我想要控制的是时间的间隔呢, 说个例子 ‘*/1****' 表示每一分钟执行一次 '*/5****' 表示每五分钟执行一次 第二部分:任务方法 是到达了时间点之后要执行的函数是谁。注意不要加括号,我们只要告诉它名字就可以了。它自己去调用。 第三部分:执行日志
是在执行第二部分的函数的时候,有可能要向终端输出信息,如果要向终端输出信息的话,需不需要把这些信息保存到一个地方,所以第三个地方就是保存到哪里
需要注意的是">>"后的空格不要忘记。
关于尖括号的科普:
在脚本写数据库数据添加的脚本中,我们也用过尖括号,尖括号的尖朝向哪里,相反方向的数据就传到哪里。
一个大括号代表覆盖,两个大括号代表追加
在定时任务中,如果出现非英文字符,会出现字符异常错误
为了避免这个问题,我们要打上双保险
第一,要在文件写的时候,指定编码为utf-8
第二,通过在配置文件中添加定时任务执行的附加命令,意思是涉及到的所有语言,都以utf-8的形式来进行处理。
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
# 解决crontab中文问题 CRONTAB_COMMAND_PREFIX = 'LANG_ALL=zh_cn.UTF-8'
开启定时任务
添加定时任务到系统中, 这个crontab有是因为前面注册了app。
python manage.py crontab add
显示已经激活的定时任务
python manage.py crontab show
移除定时任务
python manage.py crontab remove
商品详情页的静态化
在添加或许修改商品的时候,用的是admin,而admin是django维护的。所以像商品详情页生成静态化页面能不能在修改完商品信息,然后点击保存之后,嵌入我们生成静态化页面的操作呢。如果加入进去的话就是每次商家点了保存之后,就会卡几秒,后台要进行数据库的查询,然后渲染,这些是很费时间的。而关于费时操作化,可以用异步任务来解决。我们只需要用户点保存之后触发这个异步任务就可以了。
考虑点:在商品详情页中有一个商品分类,我们需要把它提出去,原因是别的页面也可能有页面静态化,它也有商品分类,而商品分类则需要查询数据库,为了减少查询,所以我们需要把它提出来,作为一个单独的函数来进行处理。
先来实现静态化异步任务,在celery_tasks中新建html/tasks.py任务
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
from celery_tasks.main import celery_app from django.template import loader from django.conf import settings import os from goods.utils import get_categories from goods.models import SKU @celery_app.task(name='generate_static_sku_detail_html') def generate_static_sku_detail_html(sku_id): """ 生成静态商品详情页面 :param sku_id: 商品sku id """ # 商品分类菜单 categories = get_categories() # 获取当前sku的信息 sku = SKU.objects.get(id=sku_id) 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 = [] for spec in s_specs: key.append(spec.option.id) # 向规格参数-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') # /front_end_pc/goods/1.html 2.html with open(file_path, 'w') as f: f.write(html_text)
对上面内容的分析:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
def generate_static_sku_detail_html(sku_id): """ 生成静态商品详情页面 :param sku_id: 商品sku id """ # 商品分类菜单 categories = get_categories() # 获取当前sku的信息 sku = SKU.objects.get(id=sku_id) # 获取到具体商品的id,比如说是华为手机,下面用sku来表示 sku.images = sku.skuimage_set.all() # 将sku所以的商品图片找出来 # 面包屑导航信息中的频道 goods = sku.goods # 获取sku对应的spu对象 goods.channel = goods.category1.goodschannel_set.all()[0] # 获取spu的商品频道对象 # 构建当前商品的规格键 sku_specs = sku.skuspecification_set.order_by('spec_id') # 获取当前sku和其商品规格和规格选项组成的那个多对多表的对象。并以规格的id进行排序。 # 在后面的map中也是这样的,这样可以保证,规格的顺序,后期好比较。 sku_key = [] # 我们要形成一个列表,用来将sku中用户在规格中所选的选项存储起来,方便以后再map中查找对应的具体商品 # 样式如:sku_key = [规格1的参数id,规格2的参数id,。。] for spec in sku_specs: sku_key.append(spec.option.id) # 获取规格的选项id添加到sku_key中 # 获取当前商品的所有SKU skus = goods.sku_set.all() # 获取获取spu中所有的sku的对象 # 构建不同规格参数(选项)的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 = [] for spec in s_specs: key.append(spec.option.id) # 向规格参数-sku字典添加记录 spec_sku_map[tuple(key)] = s.id # 获取当前商品的规格信息 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') as f: f.write(html_text)
在处理的时候我们应该每个商品详情对应一个页面,所以在后面文件保存的时候,我们做了用商品id来做静态化页面名字进行拼接的操作。
所以,在front_end_pc中新建goods文件夹。
我们把这个商品分离分离到了get_categories这个函数中去了。然后对这个函数进行增加。
将形成商品类别部分的数据封装成一个公共函数,放在goods/utils.py中
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
from collections import OrderedDict from .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
最后,不要忘了修改异步任务中的main.py将,这个任务加到任务中去。
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
# 导入任务 celery_app.autodiscover_tasks(['celery_tasks.sms', 'celery_tasks.email', 'celery_tasks.html'])
然后是在admin中点击保存,执行这个生成静态化页面的异步任务了。
在admin中,我们可以自定义一个admin样式,具体定义方法,看admin的相关内容。除了相应的样式设置,我们还可以重写两个方法。
在Admin站点保存或者删除数据的时候,Django是调用的admin站点管理器类的save_model()方法和delete_model()方法,我们只需要重新实现这两个方法,在这两个方法中调用异步任务即可。当我们保存或者删除的化,django就知道我们操作的是哪个模型类的对象。
save_model()方法接收四个参数:
- request,当你点击保存的时候的请求对象,
- obj,当前保存的数据对应的模型类对象是谁,它会将我们这个对象中,也就是页面中(新增或者修改的时候的那个页面)填写的数据提取出来,直接把在注册的时候对应的模型类和相应的数据创建出来一个模型类对象。所以简单说,obj就是有了这些属性的对象。它还没有到数据库中,只是创建出来了一个对象。
- form, 把原始页面中(新增或者修改的时候的那个页面)的数据提出来,就放到了form中了。
- change,在这次保存时候,改变数据的地方。
在源码中,save_model()里面就有obj.save()这一个命令。而我们要使用的目前也只有obj。我们也执行保存,只不过保存后加入执行异步任务的命令。
delete_model()方法接收两个参数:request和obj。对应的就是请求对象和要删除的对象,我们也就和源码一样obj.delete()就行了。
编辑goods/admin.py,把所有需要调整的都进行增加。
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
from django.contrib import admin # Register your models here. from . import models class SKUAdmin(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.id) class SKUSpecificationAdmin(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) 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 -> SKUImage 对象 obj.sku 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: # http://image.meiduo.site:8888/groupxxxxxx 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(models.GoodsCategory) admin.site.register(models.GoodsChannel) admin.site.register(models.Goods) admin.site.register(models.Brand) admin.site.register(models.GoodsSpecification) admin.site.register(models.SpecificationOption) admin.site.register(models.SKU, SKUAdmin) admin.site.register(models.SKUSpecification, SKUSpecificationAdmin) admin.site.register(models.SKUImage, SKUImageAdmin)
注:在商品SKU表中有一个默认图片,我们要如果修改了SKU表中的数据,并没有修改商品SKU表中的默认图片字段,所有我们顺便在admin中进行了修改。将第一个上传的SKU图片作为商品的默认图片。
在template中加入商品详情页的模板detail.html
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
<!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="/css/reset.css"> <link rel="stylesheet" type="text/css" href="/css/main.css"> <script type="text/javascript" src="/js/host.js"></script> <script type="text/javascript" src="/js/vue-2.5.16.js"></script> <script type="text/javascript" src="/js/axios-0.18.0.min.js"></script> </head> <body> <div id="app" v-clo1ak> <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> <span>|</span> <a @click="logout">退出</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="/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="#" class="cart_name fl">我的购物车</a> <div class="goods_count fl" id="show_count">15</div> <ul class="cart_goods_show"> <li> <img src="images/goods/goods001.jpg" alt="商品图片"> <h4>商品名称手机</h4> <div>4</div> </li> <li> <img src="images/goods/goods002.jpg" alt="商品图片"> <h4>商品名称手机</h4> <div>5</div> </li> <li> <img src="images/goods/goods003.jpg" alt="商品图片"> <h4>商品名称手机</h4> <div>6</div> </li> <li> <img src="images/goods/goods003.jpg" alt="商品图片"> <h4>商品名称手机</h4> <div>6</div> </li> </ul> </div> </div> <div class="navbar_con"> <div class="navbar"> <div class="sub_menu_con fl"> <h1 class="fl">商品分类</h1> <ul class="sub_menu"> {% for group in categories.values %} <li> <div class="level1"> {% for channel in group.channels %} <a href="{{ channel.url }}">{{ channel.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> <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="breadcrumb"> <a href="{{ goods.channel.url }}">{{ goods.category1.name }}</a> <span>></span> <span>{{ goods.category2.name }}</span> <span>></span> <a href="/list.html?cat={{ goods.category3.id }}">{{goods.category3.name }}</a> </div> <div class="goods_detail_con clearfix"> <div class="goods_detail_pic fl"><img src="{{ sku.default_image_url }}"></div> <div class="goods_detail_list fr"> <h3>{{ sku.name }}</h3> <p>{{ sku.caption }}</p> <div class="prize_bar"> <span class="show_pirze">¥<em>{{ sku.price }}</em></span><span> 市场价¥{{sku.market_price}}</span> <a href="javascript:;" class="goods_judge">{{ sku.comments }}人评价</a> </div> <div class="goods_num clearfix"> <div class="num_name fl">数 量:</div> <div class="num_add fl"> <input v-model="sku_count" type="text" class="num_show fl"> <a @click="sku_count++" class="add fr">+</a> <a @click="on_minus()" class="minus fr">-</a> </div> </div> {% for spec in specs %} <div class="type_select"> <label>{{ spec.name }}:</label> {% for option in spec.options %} {% if option.sku_id == sku.id %} <a href="javascript:;" class="select">{{ option.value }}</a> {% elif option.sku_id %} <a href="/goods/{{option.sku_id}}.html">{{ option.value }}</a> {% else %} <a href="javascript:;">{{ option.value }}</a> {% endif %} {% endfor %} </div> {% endfor %} <div class="total">总价:<em>[[sku_amount]]元</em></div> <div class="operate_btn"> <a @click="add_cart" class="add_cart" id="add_cart">加入购物车</a> </div> </div> </div> <div class="main_wrap clearfix"> <div class="l_wrap fl clearfix"> <div class="new_goods"> <h3>热销排行</h3> <ul> <li v-for="sku in hots"> <a :href="sku.url"><img :src="sku.default_image_url"></a> <h4><a :href="sku.url">[[sku.name]]</a></h4> <div class="prize">¥[[sku.price]]</div> </li> </ul> </div> </div> <div class="r_wrap fr clearfix"> <ul class="detail_tab clearfix"> <li @click="on_tab_content('detail')" :class="tab_content.detail?'active':''">商品详情</li> <li @click="on_tab_content('pack')" :class="tab_content.pack?'active':''">规格与包装</li> <li @click="on_tab_content('comment')" :class="tab_content.comment?'active':''">商品评价([[comments.length]])</li> <li @click="on_tab_content('service')" :class="tab_content.service?'active':''">售后服务</li> </ul> <div @click="on_tab_content('detail')" class="tab_content" :class="tab_content.detail?'current':''"> <dl> <dt>商品详情:</dt> <dd>{{ goods.desc_detail|safe }}</dd> </dl> </div> <div @click="on_tab_content('pack')" class="tab_content" :class="tab_content.pack?'current':''"> <dl> <dt>规格与包装:</dt> <dd>{{ goods.desc_pack|safe }}</dd> </dl> </div> <div @click="on_tab_content('comment')" class="tab_content" :class="tab_content.comment?'current':''"> <ul class="judge_list_con"> <li class="judge_list fl" v-for="comment in comments"> <div class="user_info fl"> <b>[[comment.username]]</b> </div> <div class="judge_info fl"> <div :class="comment.score_class"></div> <div class="judge_detail">[[comment.comment]]</div> </div> </li> </ul> </div> <div @click="on_tab_content('service')" class="tab_content" :class="tab_content.service?'current':''"> <dl> <dt>售后服务:</dt> <dd>{{ goods.desc_service|safe }}</dd> </dl> </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"> var price = {{sku.price}}; var cat = {{ goods.category3.id }}; </script> <script type="text/javascript" src="/js/detail.js"></script> </body> </html>
在js中加入detail.js
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
/** * Created by python on 18-7-11. */ var vm = new Vue({ el: '#app', delimiters: ['[[', ']]'], data: { host, username: sessionStorage.username || localStorage.username, user_id: sessionStorage.user_id || localStorage.user_id, token: sessionStorage.token || localStorage.token, tab_content: { detail: true, pack: false, comment: false, service: false }, sku_id: '', sku_count: 1, sku_price: price, cart_total_count: 0, // 购物车总数量 cart: [], // 购物车数据 hots: [], // 热销商品 cat: cat, // 商品类别 comments: [], // 评论信息 score_classes: { 1: 'stars_one', 2: 'stars_two', 3: 'stars_three', 4: 'stars_four', 5: 'stars_five', } }, computed: { sku_amount: function(){ return (this.sku_price * this.sku_count).toFixed(2); } }, mounted: function(){ // 添加用户浏览历史记录 this.get_sku_id(); this.get_cart(); this.get_hot_goods(); this.get_comments(); }, methods: { // 退出 logout: function(){ sessionStorage.clear(); localStorage.clear(); location.href = '/login.html'; }, // 控制页面标签页展示 on_tab_content: function(name){ this.tab_content = { detail: false, pack: false, comment: false, service: false }; this.tab_content[name] = true; }, // 从路径中提取sku_id get_sku_id: function(){ var re = /^\/goods\/(\d+).html$/; this.sku_id = document.location.pathname.match(re)[1]; }, // 减小数值 on_minus: function(){ if (this.sku_count > 1) { this.sku_count--; } }, // 添加购物车 add_cart: function(){ }, // 获取购物车数据 get_cart: function(){ }, // 获取热销商品数据 get_hot_goods: function(){ axios.get(this.host+'/goods/categories/'+this.cat+'/hotskus/', { responseType: 'json' }) .then(response => { this.hots = response.data; for(var i=0; i<this.hots.length; i++){ this.hots[i].url = '/goods/' + this.hots[i].id + '.html'; } }) .catch(error => { console.log(error.response.data); }) }, // 获取商品评价信息 get_comments: function(){ } } });
为了开发方便,我们还可以编写手动生成所有商品静态页面的脚本regenerate_detail_html.py
脚本应该是独立的,就是我一运行这个脚本,它就开始执行django,然后把我们需要的页面进行静态化。所有我们在使用静态化的函数之前,要进行django的环境配置。
添加可执行权限。
在scripts中新建
生成首页的脚本regenerate_index_html.py
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
#!/usr/bin/env python import sys sys.path.insert(0, '../') import os if not os.getenv('DJANGO_SETTINGS_MODULE'): os.environ['DJANGO_SETTINGS_MODULE'] = 'meiduo_mall.settings.dev' # 让django初始化 import django django.setup() from contents.crons import generate_static_index_html if __name__ == '__main__': generate_static_index_html()
生成详情的脚本regenerate_detail_html.py
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
#!/usr/bin/env python """ 功能:手动生成所有SKU的静态detail html文件 使用方法: ./regenerate_detail_html.py """ import sys sys.path.insert(0, '../') import os if not os.getenv('DJANGO_SETTINGS_MODULE'): os.environ['DJANGO_SETTINGS_MODULE'] = 'meiduo_mall.settings.dev' import django django.setup() from django.template import loader from django.conf import settings from goods.utils import get_categories from goods.models import SKU def generate_static_sku_detail_html(sku_id): """ 生成静态商品详情页面 :param sku_id: 商品sku id """ # 商品分类菜单 categories = get_categories() # 获取当前sku的信息 sku = SKU.objects.get(id=sku_id) sku.images = sku.skuimage_set.all() # 面包屑导航信息中的频道 goods = sku.goods goods.channel = goods.category1.goodschannel_set.all()[0] # 构建当前商品的规格键 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 = [] for spec in s_specs: key.append(spec.option.id) # 向规格参数-sku字典添加记录 spec_sku_map[tuple(key)] = s.id # 获取当前商品的规格信息 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') as f: f.write(html_text) if __name__ == '__main__': skus = SKU.objects.all() for sku in skus: print(sku.id) generate_static_sku_detail_html(sku.id)
生成详情页静态页面的时候,详情页面的静态化是在异步中的tasks中完成的,我是用脚本没有必要去开启异步任务,所以直接把异步任务中除了装饰器的都拷走就行了。
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
#!/usr/bin/env python 这样声明的时候,会去取你机器的 PATH 中指定的第一个 python 来执行你的脚本。 #!/usr/bin/python 表示写死了就是要 /usr/bin/python 这个目录下 python 来执行你的脚本。这样写程序的可移植性就差了,如果此路径下python命令不存在就会报错。 所以一般情况还是用第一种写法。
添加可执行权限
chmod +x regenerate_static_index_html.py
chmod +x regenerate_static_detail_html.py
然后执行
./regenerate_static_detail_html.py
将所有的测试数据都生成模板