一、收藏功能的数据模型设计
收藏功能的核心是:一个用户可以收藏多篇文章,一篇文章可以被多个用户收藏。这是典型的多对多(ManyToMany)关系。
我们不需要新建模型——Django 的 User 模型是内置的,只需要在 Post 模型中添加一个 ManyToManyField 指向 User。
python
# 文件路径:blog/models.py 的 Post 类中添加
from django.contrib.auth.models import User
class Post(models.Model):
# … 已有字段 …
# 收藏者:ManyToManyField 自动创建中间表
favorites = models.ManyToManyField(
User,
related_name='favorite_posts', # 反向查询:user.favorite_posts.all()
verbose_name='收藏者',
blank=True
)
参数说明:
| User | 关联的目标模型 |
| related_name='favorite_posts' | 反向查询名称,user.favorite_posts.all() |
| verbose_name='收藏者' | Admin 后台显示的中文名 |
| blank=True | 表单中允许为空 |
中间表说明:
ManyToManyField 会自动创建一张中间表(如 blog_post_favorites),包含 post_id 和 user_id 两个外键字段,无需手动定义。
修改模型后,生成并执行迁移:
bash
(venv) $ python manage.py makemigrations
(venv) $ python manage.py migrate
二、收藏与取消收藏的视图逻辑
python
# 文件路径:blog/views.py 新增
from django.shortcuts import render, redirect, get_object_or_404
from django.contrib.auth.decorators import login_required
from django.contrib import messages
from .models import Post
# login_required 装饰器:未登录用户访问此视图时,自动跳转到登录页
@login_required
def toggle_favorite(request, pk):
"""切换收藏状态:收藏 → 取消,未收藏 → 收藏"""
post = get_object_or_404(Post, pk=pk)
# 检查当前用户是否已收藏
if post.favorites.filter(id=request.user.id).exists():
# 已收藏 → 取消收藏
post.favorites.remove(request.user)
messages.info(request, f'已取消收藏「{post.title}」')
else:
# 未收藏 → 添加收藏
post.favorites.add(request.user)
messages.success(request, f'已收藏「{post.title}」')
# 重定向回当前页面(HTTP_REFERER 是来源页面的 URL)
referer = request.META.get('HTTP_REFERER', '/')
return redirect(referer)
关键点解析:
| @login_required | 未登录用户自动跳转到登录页 |
| get_object_or_404 | 文章不存在时返回 404 |
| post.favorites.filter(id=…).exists() | 判断是否已收藏 |
| post.favorites.remove(user) | 取消收藏 |
| post.favorites.add(user) | 添加收藏 |
| request.META.get('HTTP_REFERER', '/') | 获取来源页面 URL,重定向回去 |
关于 HTTP_REFERER: 它记录用户是从哪个页面跳转过来的。用户可能从首页或详情页点击收藏,重定向回来源页面能保持操作连贯性。如果取不到,则默认跳转首页。
三、ManyToMany 操作方法速查
| add(obj) | 添加关联 | post.favorites.add(user) |
| remove(obj) | 移除关联 | post.favorites.remove(user) |
| all() | 查询所有关联对象 | post.favorites.all() |
| filter(…) | 带条件过滤 | post.favorites.filter(id=1) |
| exists() | 判断是否有匹配记录 | post.favorites.filter(id=1).exists() |
| clear() | 清除所有关联 | post.favorites.clear() |
| count() | 统计关联数量 | post.favorites.count() |
反向查询:
python
# 通过 related_name 反向查询用户收藏的所有文章
user.favorite_posts.all()
四、配置收藏路由
python
# 文件路径:blog/urls.py 追加
urlpatterns = [
# … 已有路由 …
path('post/<int:pk>/favorite/', views.toggle_favorite, name='toggle_favorite'),
path('favorites/', views.favorites, name='favorites'),
]
五、在详情页添加收藏按钮
html
<!– 文件路径:blog/templates/blog/post_detail.html(修改) –>
{% extends 'blog/base.html' %}
{% block title %}{{ post.title }} – RUNOOB 博客{% endblock %}
{% block content %}
<article class="post-view">
<span class="category-tag">{{ post.category.name }}</span>
<div class="post-header">
<h1>{{ post.title }}</h1>
<!– 收藏按钮:仅登录用户可见 –>
{% if user.is_authenticated %}
<form method="post" action="{% url 'toggle_favorite' post.pk %}">
{% csrf_token %}
<button type="submit" class="fav-btn">
{% if user in post.favorites.all %}
♥ 已收藏
{% else %}
♡ 收藏
{% endif %}
</button>
</form>
{% endif %}
</div>
<time>{{ post.created_at|date:"Y-m-d" }}</time>
<div class="content">{{ post.content|safe }}</div>
<a href="{% url 'index' %}" class="back-link">← 返回首页</a>
</article>
{% endblock %}
判断是否已收藏的逻辑:
html
{% if user in post.favorites.all %}
user in post.favorites.all() 判断当前用户是否在收藏者列表中,从而显示不同的按钮文字和图标。
在首页文章卡片中也加入收藏按钮
html
<!– 在 index.html 的文章卡片中加入收藏按钮(footer 区域) –>
<div class="article-card">
<div class="card-content">
<span class="card-category">{{ post.category.name }}</span>
<h3>{{ post.title }}</h3>
<p>{{ post.summary|truncatechars:80 }}</p>
<div class="card-footer">
<span class="card-date">{{ post.created_at|date:"Y-m-d" }}</span>
{% if user.is_authenticated %}
<form method="post" action="{% url 'toggle_favorite' post.pk %}" class="fav-form">
{% csrf_token %}
<button type="submit" class="fav-btn-small">
{% if user in post.favorites.all %}♥{% else %}♡{% endif %}
</button>
</form>
{% endif %}
</div>
</div>
</div>
为什么收藏按钮用 form 而不是 a 标签?
因为收藏操作修改了数据库,属于 POST 请求。按照 REST 规范,读操作用 GET,写操作用 POST。更重要的是,POST 请求携带 csrf_token,防止恶意网站伪造请求。
六、收藏列表页面
为登录用户展示已收藏的文章列表。
python
# 文件路径:blog/views.py 新增
@login_required
def favorites(request):
"""当前用户的收藏列表"""
# 通过 related_name 反向查询用户的收藏文章
posts = request.user.favorite_posts.all().order_by('-created_at')
return render(request, 'blog/favorites.html', {
'posts': posts,
'title': '我的收藏 – RUNOOB 博客'
})
创建收藏列表模板:
html
<!– 文件路径:blog/templates/blog/favorites.html –>
{% extends 'blog/base.html' %}
{% block title %}{{ title }}{% endblock %}
{% block content %}
<h2 class="section-title">我的收藏</h2>
{% if posts %}
<div class="article-grid">
{% for post in posts %}
<a href="{% url 'post_detail' post.pk %}" class="card-link">
<div class="article-card">
<div class="card-content">
<span class="card-category">{{ post.category.name }}</span>
<h3>{{ post.title }}</h3>
<p>{{ post.summary|truncatechars:80 }}</p>
<span class="card-date">{{ post.created_at|date:"Y-m-d" }}</span>
</div>
</div>
</a>
{% endfor %}
</div>
{% else %}
<p class="empty-tip">你还没有收藏任何文章</p>
{% endif %}
{% endblock %}
在导航栏添加「我的收藏」链接:
html
<nav>
<a href="/">首页</a>
{% if user.is_authenticated %}
<a href="{% url 'favorites' %}">我的收藏</a>
<span class="user-name">{{ user.username }}</span>
<a href="{% url 'logout' %}">登出</a>
{% else %}
<a href="{% url 'login' %}">登录</a>
<a href="{% url 'register' %}">注册</a>
{% endif %}
</nav>
七、收藏按钮样式
在 base.html 的 style 中追加:
css
/* 文章详情页头部 */
.post-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
}
/* 收藏按钮 */
.fav-btn, .fav-btn-small {
background: none;
border: none;
font-size: 18px;
cursor: pointer;
color: #e74c3c;
padding: 4px 8px;
transition: transform 0.2s;
}
.fav-btn:hover, .fav-btn-small:hover {
transform: scale(1.2);
}
.fav-btn-small {
font-size: 16px;
}
.fav-form {
display: inline;
}
.card-footer {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 10px;
}
样式要点:
-
.fav-btn 使用红色 #e74c3c,符合"收藏/喜欢"的视觉习惯。
-
:hover 时 transform: scale(1.2) 放大,提升交互反馈。
-
.card-footer 用 flex 布局,日期和收藏按钮分居两侧。
八、收藏功能流程图
九、动手:测试收藏功能
启动服务器,验证以下几点:
| 未登录访问 | 未登录时查看详情页 | 不显示收藏按钮 |
| 登录后收藏 | 登录后点击收藏按钮 | 按钮变为"已收藏",显示成功提示 |
| 取消收藏 | 再次点击已收藏按钮 | 按钮恢复为"收藏",显示取消提示 |
| 首页收藏 | 在首页卡片点击收藏 | 状态正确切换 |
| 收藏列表 | 访问"我的收藏" | 显示已收藏的文章 |
| 空收藏 | 未收藏任何文章时访问 | 显示"你还没有收藏任何文章" |
| 未登录访问收藏列表 | 未登录访问 /favorites/ | 自动跳转到登录页 |
十、常见问题排查
| 提交报 403 | 表单缺少 CSRF 令牌 | 添加 {% csrf_token %} |
| 收藏后状态不变 | 未执行迁移 | 运行 makemigrations 和 migrate |
| related_name 冲突 | 与其他模型重名 | 修改 related_name 值 |
| 未登录点击收藏无反应 | 按钮未加登录判断 | 用 {% if user.is_authenticated %} 包裹 |
| 重定向失败 | HTTP_REFERER 为空 | 已设默认值 '/',检查是否被浏览器禁用 |
| 收藏列表为空 | 反向查询名称错误 | 检查 related_name='favorite_posts' |
| ManyToManyField 报错 | 未导入 User | 添加 from django.contrib.auth.models import User |
十一、小结
本章你掌握了 Django 中的用户交互处理:
-
多对多关系:ManyToManyField 自动创建中间表,无需手动定义。
-
POST 请求处理:收藏操作修改数据库,必须用 POST + csrf_token。
-
关联操作:add()、remove()、all()、filter()、exists()。
-
访问控制:@login_required 装饰器保护需要登录的视图。
-
当前用户:request.user 获取登录用户身份。
-
来源重定向:HTTP_REFERER 实现操作后返回原页面。
收藏功能让用户可以标记喜欢的文章,并在个人收藏列表中查看。下一篇我们将实现评论功能,让用户可以在文章下交流讨论。



