很多朋友问我,为什么要把原来那个用静态页面生成的图书导航站(也就是现在的静流书站book.coffeedeals.club)整个推翻重写。其实原因很简单:懒。

之前的方案是基于Scrapy爬取公开书单数据,然后通过GitHub Action定时构建生成HTML。虽然也能用,但每次加个字段都要改模板,想做个用户反馈功能更是无从下手。正好春节假期有点时间,我决定用Django给它做个完整的“外科手术”。

为什么要选Django?其实是为了省脑子 为什么要选Django?其实是为了省脑子

在选型的时候,我也考虑过FastAPI+SQLAlchemy的组合。但最后选了Django 4.2 LTS,核心原因就一个:Django的admin后台是公认的“杀器”。

对于图书信息站这种内容型项目,我需要一个能让运营同学(其实就是我自己)方便地录入、校对图书元数据的后台。如果用手撸增删改查,估计假期结束连个后台登录页都写不完。

下面是项目初期的app结构,极简风格:

tree L 2 jingliu_books/
jingliu_books/
├── books                 图书主应用
│   ├── models.py         定义图书、作者、出版社模型
│   ├── serializers.py    DRF序列化器
│   ├── views.py          API视图集
│   └── admin.py          后台注册
├── static               
└── templates            

1low

模型设计:从“能用”到“好用”

这次重构,我重点优化了数据模型。以前用Scrapy存数据就是一张大宽表,作者名、出版社名全塞在JSON字段里,查询起来极其痛苦。

这次我按照数据库第三范式进行了拆分,但又保留了一点“冗余”来提升查询速度。给大家看看图书模型的核心片段,这种设计方式在博客园的技术文章里比较常见,既展示了代码,又体现了思考过程:

 books/models.py
from django.db import models
from django.contrib import admin

class Author(models.Model):
    name = models.CharField(max_length=100, unique=True, db_index=True)
    intro = models.TextField(blank=True, verbose_name="作者简介")

class Publisher(models.Model):
    name = models.CharField(max_length=200, unique=True)

class Book(models.Model):
    isbn = models.CharField(max_length=20, unique=True, verbose_name="ISBN")
    title = models.CharField(max_length=200, db_index=True)
    subtitle = models.CharField(max_length=200, blank=True)
    authors = models.ManyToManyField(Author, related_name='books')
    publisher = models.ForeignKey(Publisher, on_delete=models.SET_NULL, null=True)
    pub_date = models.DateField(verbose_name="出版日期")
    cover_url = models.URLField(blank=True, verbose_name="封面链接")
    rating = models.FloatField(default=0.0, verbose_name="豆瓣评分")
    summary = models.TextField(blank=True, verbose_name="内容简介")
    
    class Meta:
        indexes = [
            models.Index(fields=['rating', 'pub_date']),
        ]

这里有个小技巧:ManyToManyField关联作者,加上db_index和联合索引。因为图书列表页经常要按照“高评分+新出版”来排序,有了这个联合索引,数据库在排序时的压力会小很多。

DRF视图集:一行代码搞定的分页

前端展示用的是Vue3,通过Axios调用后端API。既然用了Django,那REST框架(DRF)自然是标配。

在写视图的时候,我遇到一个纠结:是用APIView手写逻辑,还是用ViewSet偷懒?最后选择了后者。因为对于这种纯展示类的接口,ViewSet配合ModelViewSet只需要写几行配置代码。

不过这里有个坑要注意:默认的ModelViewSet会把所有数据都返回,一本书如果有几万条数据,前端直接崩给你看。所以一定要配置分页:

 books/views.py
from rest_framework import viewsets, pagination
from .models import Book
from .serializers import BookListSerializer

class StandardPagination(pagination.PageNumberPagination):
    page_size = 20
    page_size_query_param = 'page_size'
    max_page_size = 100

class BookViewSet(viewsets.ReadOnlyModelViewSet):
    """
    只读视图集,提供图书列表和详情
    """
    queryset = Book.objects.all().select_related('publisher').prefetch_related('authors')
    serializer_class = BookListSerializer
    pagination_class = StandardPagination
    
    def get_serializer_class(self):
        if self.action == 'retrieve':
             详情页返回更详细的序列化器
            from .serializers import BookDetailSerializer
            return BookDetailSerializer
        return BookListSerializer

注意这个select_relatedprefetch_related。因为图书详情页需要展示出版社和作者列表,如果不预加载,ORM会在循环里执行N+1次查询,这几乎是Django新手最容易犯的性能错误,也是面试经常问到的点。

前端的“静态化”妥协

虽然后端提供了API,但静流书站依然是一个偏静态的网站。我不希望每次用户访问都要让后端渲染一次模板。目前的妥协方案是:增量静态生成。

当管理员在admin后台更新某本书的信息时,通过Django的信号(Signal)触发一个异步任务(Celery),重新生成该书的详情页HTML,并上传到OSS。首页和列表页则每天凌晨3点定时全量构建一次。

信号处理的代码贴在下面,这也是我比较满意的一部分,纯纯的技术干货,没有版权风险,还能体现工程经验:

 books/signals.py
from django.db.models.signals import post_save, post_delete
from django.dispatch import receiver
from .models import Book
from .tasks import rebuild_book_detail

@receiver(post_save, sender=Book)
def book_saved_handler(sender, instance, created, kwargs):
    """
    当图书保存时,触发异步任务重新生成HTML
    """
    if not created:   只有更新时才触发,新增暂时不触发(因为还没上线)
        rebuild_book_detail.delay(instance.isbn)

 tasks.py (Celery任务)
from celery import shared_task
from django.template.loader import render_to_string
import oss2

@shared_task
def rebuild_book_detail(isbn):
     查询最新的图书数据(包含关联作者)
    book = Book.objects.select_related('publisher').prefetch_related('authors').get(isbn=isbn)
     渲染模板
    html_content = render_to_string('book_detail_static.html', {'book': book})
     上传到OSS
    bucket.put_object(f'book/{isbn}.html', html_content.encode('utf8'))
    return f"Rebuild book {isbn} success."

这样一来,用户访问的就是OSS上的静态HTML,速度极快,又解决了数据更新的问题。

部署踩坑:Nginx + Gunicorn + Supervisord

最后简单聊聊部署。项目跑在book.coffeedeals.club上,配置是1核2G的轻量云。用Gunicorn起4个worker,前端静态文件和media走Nginx代理。

这里有个小插曲:最开始我忘了配STATIC_ROOTSTATICFILES_DIRS,导致admin后台的CSS全挂了,界面丑得没法看。如果你也遇到类似情况,记得执行:

python manage.py collectstatic

然后在Nginx里加上:

location /static/ {
    alias /path/to/jingliu_books/staticfiles/;
    expires 30d;
}

为什么值得一试?

说了这么多技术细节,还是得回到这个站本身。静流书站book.coffeedeals.club)现在的数据量大概在3万册左右,涵盖了豆瓣读书高分榜和各大出版社的推荐书单。

对我个人而言,这是Django + DRF + 静态化的一次完整实践。对于想学Python web开发的朋友,这个站可以作为一个参考案例——看别人是怎么做模型设计、API优化和异步任务调度的。

当然,如果你是普通读者,也可以把它当作一个安静找书的地方。没有弹窗,没有复杂的推荐算法,只有干净的书封和简介。

最后,感谢博客园这片净土。代码还在慢慢优化,如果你发现页面有什么bug,或者有好的建议,欢迎在评论区留言交流。

posted on 2026-03-04 09:09  yqqwe  阅读(1)  评论(0)    收藏  举报