Add Day_4: 设计一个真正的Django小应用

This commit is contained in:
2026-08-18 00:10:54 +08:00
parent 7dfc4c63c5
commit cbd5eaf357
2 changed files with 315 additions and 0 deletions
+203
View File
@@ -0,0 +1,203 @@
# 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/<int:pk>/", views.article_detail, name="article_detail"),
path("article/<int:pk>/comment/", views.add_comment, name="add_comment"),
path("author/<str:nickname>/", 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/<int:pk>/")`,把 `pk=3` 提取出来并调用对应的**视图函数**
> `article_detail(request, pk=3)`。视图内执行 **ORM 查询**(`get_object_or_404` 对应
> 一条 SELECT),拿到文章对象后调用 **`render`** 把数据填入模板、生成 HTML,
> 最终打包成 **`HttpResponse`** 返回给浏览器渲染。
---
## 5. 进阶:index.html 模板片段
```html
{% if articles %}
<ul>
{% for a in articles %}
<li>
<a href="{% url 'blog:article_detail' a.pk %}">{{ a.title }}</a>
<span>{{ a.created_at|date:"Y-m-d H:i" }}</span>
· 作者:{{ a.author.nickname }}
</li>
{% endfor %}
</ul>
{% else %}
<p>暂无文章,快来发布第一篇吧!</p>
{% endif %}
```
---
## 验收清单
- [ ] `python manage.py makemigrations blog` 与 `migrate` 无报错
- [ ] `runserver` 后,列表页 / 详情页 / 评论提交 / 作者页全部可访问
- [ ] 删掉一个 Member 后,他的文章和评论也一起消失(验证 CASCADE)
- [ ] 能口头讲清楚:两个用户同时点文章,浏览量不会"丢访问"