一、文档下载功能
1.分析
业务处理流程:
- 判断前端传的文件id是否为空,对应的文件是否存在
请求方法:GET
url定义:/docs/<int:doc_id>/
请求参数:url路径参数
参数 | 类型 | 前端是否必须传 | 描述 |
---|---|---|---|
doc_id | 整数 | 是 | 文件id |
此功能是通过向前端返回FileResponse来实现的。
2.后端代码实现
# 导入测试数据
insert into tb_docs (file_url, title, `desc`, image_url, author_id, create_time, update_time, is_delete) values
('media/流畅的Python.pdf', '流畅的Python', '本书由奋战在Python开发一线近20年的Luciano Ramalho执笔,从语言设计层面剖析编程细节,兼顾Python 3和Python 2,教你写出风格地道的Python代码。', 'media/fluent_python_1.jpg', 1, now(), now(), 0),
('media/笨办法学python.pdf', '笨办法学python', '本书的目的是让你起步编程。虽然说是用“Hard Way”(笨办法)学习写程序,但其实并非如此。所谓的“笨办法”指的是本文的教学方式,也就是所谓的“指令式”教学。', 'media/hard_way_to_learn_python.jpg', 1, now(), now(), 0),
('media/像计算机科学家一样思考Python.pdf', '像计算机科学家一样思考Python', '《像计算机科学家一样思考python》按照培养读者像计算机科学家一样的思维方式的思路来教授python语言编程。对于第一次接触程序设计的人来说,是一本不可多得的佳作。', 'media/像计算机科学家一样思考Python.jpg', 2, now(), now(), 0),
('media/《Python+Cookbook》第三版中文v3.0.0.pdf', '《Python+Cookbook》第三版中文v3', '本书覆盖了Python应用中的很多常见问题,并提出了通用的解决方案。书中包含了大量实用的编程技巧和示例代码,并在Python 3.3环境下进行了测试,可以很方便地应用到实际项目中去。', 'media/Python_Cookbook_第三版.jpg', 2, now(), now(), 0),
('media/Python核心编程(第3版)PDF高清晰完整中文版.pdf', 'Python核心编程(第3版)PDF高清晰完整中文版', '是经典畅销图书《Python核心编程(第二版)》的全新升级版本,总共分为3部分。适合具有一定经验的Python开发人员阅读。', 'media/Python核心编程.jpg', 1, now(), now(), 0),
('media/django项目班_英语单词2.doc', 'django项目班_英语单词2', 'youkou老师说:每天一个单词,充实每一天!', 'media/django项目班_英语单词.jpg', 2, now(), now(), 0);
# 在apps/doc/models.py中定义数据库模型
from django.db import models
from utils.BaseModel.models import ModelBase
class Doc(ModelBase):
"""
create doc view
"""
file_url = models.URLField(verbose_name="文件url", help_text="文件url")
title = models.CharField(max_length=150, verbose_name="文档标题", help_text="文档标题")
desc = models.TextField(verbose_name="文档描述", help_text="文档描述")
image_url = models.URLField(default="", verbose_name="图片url", help_text="图片url")
author = models.ForeignKey('users.Users', on_delete=models.SET_NULL, null=True)
class Meta:
db_table = "tb_docs" # 指明数据库表名
verbose_name = "文件" # 在admin站点中显示的名称
verbose_name_plural = verbose_name # 显示的复数名称
def __str__(self):
return self.title
# 在apps/doc/views.py中定义如下两个视图:
import requests
import logging
from django.http import FileResponse, Http404
from django.utils.encoding import escape_uri_path
from django.views import View
from django.shortcuts import render
from Dreamblog import settings
from .models import Doc
logger = logging.getLogger('django')
class DocIndexView(View):
"""
"""
def get(self, request):
"""
渲染文章下载主页
:param request:
:return:
"""
docs = Doc.objects.defer('author', 'create_time', 'update_time', 'is_delete').filter(is_delete=False)
return render(request, 'docs/docDownload.html', locals())
class DocDownloadView(View):
"""
"""
def get(self, request, doc_id):
doc = Doc.objects.only('file_url').filter(is_delete=False, id=doc_id).first()
if doc:
doc_url = doc.file_url
# 拼接路径
doc_url = settings.SITE_DOMAIN_PORT + doc_url
doc_name = doc.title
try:
res = FileResponse(requests.get(doc_url, stream=True))
# 仅测试的话可以这样子设置
# res = FileResponse(open(doc.file_url, 'rb'))
except Exception as e:
logger.info("获取文档内容出现异常:\n{}".format(e))
raise Http404("文档下载异常!")
ex_name = doc_url.split('.')[-1]
# https://stackoverflow.com/questions/23714383/what-are-all-the-possible-values-for-http-content-type-header
# http://www.iana.org/assignments/media-types/media-types.xhtml#image
if not ex_name:
raise Http404("文档url异常!")
else:
ex_name = ex_name.lower()
if ex_name == "pdf":
res["Content-type"] = "application/pdf"
elif ex_name == "zip":
res["Content-type"] = "application/zip"
elif ex_name == "doc":
res["Content-type"] = "application/msword"
elif ex_name == "xls":
res["Content-type"] = "application/vnd.ms-excel"
elif ex_name == "docx":
res["Content-type"] = "application/vnd.openxmlformats-officedocument.wordprocessingml.document"
elif ex_name == "ppt":
res["Content-type"] = "application/vnd.ms-powerpoint"
elif ex_name == "pptx":
res["Content-type"] = "application/vnd.openxmlformats-officedocument.presentationml.presentation"
else:
raise Http404("文档格式不正确!")
# 将文件名进行中文编码
doc_filename = escape_uri_path(doc_url.split('/')[-1])
# 设置为inline,下载后会直接打开(预览操作) 设置为attachment,会直接下载
# 这个头信息代表这,点击下载会进行怎样的处理
# filename*=UTF-8''{}" 显示下载文件的中文名
res["Content-Disposition"] = "attachment; filename*=UTF-8''{}".format(doc_filename)
return res
else:
raise Http404("文档不存在!")
优化(增加下载速度):
1 . stream=True 当我初次请求时,不会下载。当需要时,才会加载文件流,渲染时,会将大量的文件保存到内存中,增大压力
# 在dj_pre_class/settings.py中加入如下配置
# 站点域名和端口配置
SITE_DOMAIN_PORT = "http://127.0.0.1:8000/"
# 在apps/doc/urls.py中定义如下url
from django.urls import path
from . import views
app_name = 'docs'
urlpatterns = [
path('download/', views.DocIndexView.as_view(), name='docs_download'),
path('download/<int:doc_id>/', views.DocDownloadView.as_view(), name='docs_download_doc'),
]
3.前端代码实现
# 在templates/doc/docDownload.html中
<div class="main-contain ">
<div class="banner">
<img src="/media/doc_banner_2.jpg"
alt="">
</div>
<div class="pay-doc-contain">
<ul class="pay-list">
{% for doc in docs %}
<li class="pay-item">
{# /{{ doc.image_url }} 保证路径正确 #}
<img src="/{{ doc.image_url }}" alt="" class="pay-img doc">
<div class="d-contain">
<p class="doc-title">{{ doc.title }}</p>
<p class="doc-desc">{{ doc.desc }}</p>
<!-- /www/?xxx -->
<a href="{% url 'doc:doc_download_doc' doc.id %}" class="pay-price">下载</a>
</div>
</li>
{% endfor %}
</ul>
</div>
</div>
把测试数据和测试文件以及相关图片文件拷贝放到media中。
/* 在static/css/doc/docDownload.css中,把如下代码覆盖之前的样式 */
/* ================= main start ================= */
#main {
margin-top: 25px;
min-height: 700px;
flex: 1;
}
/* ========= main-contain start ============ */
#main .main-contain {
width: 800px;
float: left;
margin-bottom: 30px;
}
/* ========= banner start =========== */
.main-contain .banner {
width: 100%;
}
.main-contain .banner img {
max-width: 100%;
}
.main-contain .pay-doc-contain {
background: #fff;
}
.main-contain .pay-doc-contain .pay-list {
display: flex;
justify-content: space-between;
flex-flow: wrap;
padding: 0 20px 20px;
}
.main-contain .pay-doc-contain .pay-item {
width: 800px;
height: 200px;
border-top: 1px solid #ddd;
margin-top: 20px;
display: flex;
}
.main-contain .pay-doc-contain .pay-item:hover {
box-shadow: 2px 2px 2px #ccc;
}
.pay-doc-contain .pay-item .pay-img {
width: 120px;
margin-right: 30px;
}
.pay-doc-contain .pay-item .pay-contain {
width: 250px;
position: relative
}
.pay-item .pay-contain .pay-title {
font-size: 20px;
line-height: 40px;
white-space: nowrap;
overflow: hidden;
}
.pay-item .pay-contain .pay-desc {
line-height: 20px;
color: #878787;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
font-size: 14px;
overflow: hidden;
}
.pay-item .pay-price {
display: block;
font-size: 20px;
text-align: right;
padding-right: 20px;
color: coral;
}
.d-contain {
width: 100%;
margin: 20px 0 20px ;
font-size: 18px;
line-height: normal;
}
.d-contain .doc-desc {
text-indent: 2em;
margin: 15px;
/*padding: 10px;*/
}
.d-contain .doc-title {
font-size: 20px;
font-weight: bold;
color: chocolate;
}
/* ========= banner end =========== */
/* ========= main-contain end ============ */
/* ================= main end ================= */
网友评论