# Day_4 参考解答 > 说明:这里是完整参考实现,按文件拆成四块。 > 建议先自己写,卡住了再来看对应文件;看完后尝试不参考、重写一遍 views.py。 --- ## 1. blog/models.py ```python from django.db import models class Member(models.Model): """社团成员""" nickname = models.CharField(max_length=50) email = models.EmailField(unique=True) # 邮箱唯一 joined_at = models.DateTimeField(auto_now_add=True) # 创建时自动记录 bio = models.TextField(blank=True, null=True) # 允许为空 def __str__(self): return self.nickname class Article(models.Model): """文章""" title = models.CharField(max_length=100) content = models.TextField() author = models.ForeignKey( Member, on_delete=models.CASCADE, # 作者被删 => 文章一起删 related_name="articles", # member.articles.all() 能取到该作者全部文章 ) is_published = models.BooleanField(default=False) created_at = models.DateTimeField(auto_now_add=True) views = models.PositiveIntegerField(default=0) # 浏览量不会为负,用 PositiveIntegerField class Meta: ordering = ["-created_at"] # 新文章在前 def __str__(self): return self.title class Comment(models.Model): """评论""" content = models.TextField() article = models.ForeignKey( Article, on_delete=models.CASCADE, related_name="comments", # article.comments.all() 能取到该文章全部评论 ) commenter = models.ForeignKey(Member, on_delete=models.CASCADE) class Meta: ordering = ["id"] # 按评论先后排序 def __str__(self): return f"评论({self.commenter}):{self.content[:20]}" ``` **思考题答案(供对照)**: 1. `on_delete=models.CASCADE`:作者被删除时,他写的文章(以及文章下的评论)**级联删除**, 不会留下指向不存在作者的孤儿数据。若改成 `SET_NULL`,删作者时文章的 `author` 会被置为 `NULL`——但前提是外键字段允许为空,即 `author = models.ForeignKey(Member, null=True, blank=True, on_delete=models.SET_NULL)`。 2. `related_name="comments"`:定义**反向查询**的名字。有了它,从文章查评论就是 `article.comments.all()`(不需要自己再写 `Comment.objects.filter(article=...)`)。 --- ## 2. blog/views.py ```python from django.db.models import Count, F from django.shortcuts import render, get_object_or_404, redirect from django.http import Http404 from .models import Article, Comment, Member def index(request): # 只要已发布文章,按时间倒序(Meta.ordering 已生效,order_by 可省略,写上更明确) articles = Article.objects.filter(is_published=True).order_by("-created_at") return render(request, "blog/index.html", {"articles": articles}) def article_detail(request, pk): # get_object_or_404 = get() + 捕获 DoesNotExist 后抛 Http404 article = get_object_or_404(Article, pk=pk) # 浏览量 +1。为什么不用 article.views += 1; article.save()? # 两个用户同时点进来:两人都读到 views=10,各自 +1 后都写回 11, # 丢了 1 次访问——这就是"并发丢失更新"。 # 用 F("views") + 1 是在数据库层完成"读旧值+1再写回", # 原子操作,不会丢更新。 Article.objects.filter(pk=pk).update(views=F("views") + 1) return render(request, "blog/detail.html", {"article": article}) def add_comment(request, pk): article = get_object_or_404(Article, pk=pk) if request.method == "POST": content = request.POST.get("content", "").strip() if content: member = Member.objects.first() # 简化:还没做登录,先随便取一个成员 Comment.objects.create( content=content, article=article, commenter=member, ) return redirect("article_detail", pk=pk) # POST 后重定向,避免刷新重复提交 def author_articles(request, nickname): author = get_object_or_404(Member, nickname=nickname) # 进阶:annotate 在数据库层完成"按文章分组数评论数", # 返回的每篇文章都带一个动态属性 comment_count。 articles = ( Article.objects.filter(author=author) .annotate(comment_count=Count("comments")) # 反向查询名直接拿来用 .order_by("-created_at") ) return render(request, "blog/author.html", {"author": author, "articles": articles}) ``` --- ## 3. 路由 **项目级 mysite/urls.py**: ```python from django.contrib import admin from django.urls import path, include urlpatterns = [ path("admin/", admin.site.urls), path("", include("blog.urls")), # 把 / 开头的请求交给 blog 应用处理 ] ``` **应用级 blog/urls.py**: ```python from django.urls import path from . import views app_name = "blog" # 配合 name 使用:模板里 {% url 'blog:article_detail' a.pk %} urlpatterns = [ path("", views.index, name="index"), path("article//", views.article_detail, name="article_detail"), path("article//comment/", views.add_comment, name="add_comment"), path("author//", views.author_articles, name="author_articles"), ] ``` **路由命名与反向解析**:`name` 给路由起了名字,模板和视图里用 `{% url 'blog:article_detail' a.pk %}` / `redirect("article_detail", pk=pk)` 引用, 而不是手写死 URL 字符串——这样以后改路径,代码不用跟着改。 --- ## 4. 完整链路说明(题目三第 2 问示例答案) > 用户在浏览器输入 `/article/3/` 回车后:Django 首先在 **URL 路由表**中逐条匹配, > 命中 `path("article//")`,把 `pk=3` 提取出来并调用对应的**视图函数** > `article_detail(request, pk=3)`。视图内执行 **ORM 查询**(`get_object_or_404` 对应 > 一条 SELECT),拿到文章对象后调用 **`render`** 把数据填入模板、生成 HTML, > 最终打包成 **`HttpResponse`** 返回给浏览器渲染。 --- ## 5. 进阶:index.html 模板片段 ```html {% if articles %}
    {% for a in articles %}
  • {{ a.title }} {{ a.created_at|date:"Y-m-d H:i" }} · 作者:{{ a.author.nickname }}
  • {% endfor %}
{% else %}

暂无文章,快来发布第一篇吧!

{% endif %} ``` --- ## 验收清单 - [ ] `python manage.py makemigrations blog` 与 `migrate` 无报错 - [ ] `runserver` 后,列表页 / 详情页 / 评论提交 / 作者页全部可访问 - [ ] 删掉一个 Member 后,他的文章和评论也一起消失(验证 CASCADE) - [ ] 能口头讲清楚:两个用户同时点文章,浏览量不会"丢访问"