收藏功能与后台管理
本章你将学会用多对多关系实现收藏功能,并用 sqladmin 搭建后台管理。
多对多关系设计
实例
# 文件路径:models.py 新增关联表和修改 Post 模型
from sqlalchemy import Table
# 关联表:User 和 Post 的多对多中间表
favorites = Table(
"favorites",
Base.metadata,
Column("user_id", Integer, ForeignKey("users.id"), primary_key=True),
Column("post_id", Integer, ForeignKey("posts.id"), primary_key=True),
Column("created_at", DateTime, default=datetime.utcnow)
)
from sqlalchemy import Table
# 关联表:User 和 Post 的多对多中间表
favorites = Table(
"favorites",
Base.metadata,
Column("user_id", Integer, ForeignKey("users.id"), primary_key=True),
Column("post_id", Integer, ForeignKey("posts.id"), primary_key=True),
Column("created_at", DateTime, default=datetime.utcnow)
)
执行迁移:
(venv) $ alembic revision --autogenerate -m "新增收藏关联表" (venv) $ alembic upgrade head
收藏与取消收藏路由
实例
# 文件路径:routers/favorites.py
from fastapi import APIRouter, Depends, HTTPException, Request
from sqlalchemy.orm import Session
from database import get_db
from models import Post, favorites
from routers.users import get_current_user
from fastapi.templating import Jinja2Templates
router = APIRouter(prefix="/favorites", tags=["收藏"])
templates = Jinja2Templates(directory="templates")
@router.post("/toggle/{post_id}")
def toggle_favorite(
post_id: int,
request: Request,
db: Session = Depends(get_db),
current_user = Depends(get_current_user) # 需要登录
):
"""切换收藏状态"""
post = db.query(Post).filter(Post.id == post_id).first()
if not post:
raise HTTPException(status_code=404, detail="文章不存在")
# 检查是否已收藏
is_faved = db.query(favorites).filter(
favorites.c.user_id == current_user.id,
favorites.c.post_id == post.id
).first() is not None
if is_faved:
# 取消收藏
db.execute(
favorites.delete().where(
(favorites.c.user_id == current_user.id) &
(favorites.c.post_id == post.id)
)
)
db.commit()
else:
# 添加收藏
db.execute(
favorites.insert().values(user_id=current_user.id, post_id=post.id)
)
db.commit()
from fastapi.responses import RedirectResponse
return RedirectResponse(url=request.headers.get("referer", "/"), status_code=303)
@router.get("/", name="favorites_list")
def favorites_list(
request: Request,
db: Session = Depends(get_db),
current_user = Depends(get_current_user)
):
"""我的收藏列表"""
faved_posts = db.query(Post).join(
favorites, (favorites.c.post_id == Post.id)
).filter(
favorites.c.user_id == current_user.id
).order_by(favorites.c.created_at.desc()).all()
return templates.TemplateResponse("favorites.html", {
"request": request,
"posts": faved_posts
})
from fastapi import APIRouter, Depends, HTTPException, Request
from sqlalchemy.orm import Session
from database import get_db
from models import Post, favorites
from routers.users import get_current_user
from fastapi.templating import Jinja2Templates
router = APIRouter(prefix="/favorites", tags=["收藏"])
templates = Jinja2Templates(directory="templates")
@router.post("/toggle/{post_id}")
def toggle_favorite(
post_id: int,
request: Request,
db: Session = Depends(get_db),
current_user = Depends(get_current_user) # 需要登录
):
"""切换收藏状态"""
post = db.query(Post).filter(Post.id == post_id).first()
if not post:
raise HTTPException(status_code=404, detail="文章不存在")
# 检查是否已收藏
is_faved = db.query(favorites).filter(
favorites.c.user_id == current_user.id,
favorites.c.post_id == post.id
).first() is not None
if is_faved:
# 取消收藏
db.execute(
favorites.delete().where(
(favorites.c.user_id == current_user.id) &
(favorites.c.post_id == post.id)
)
)
db.commit()
else:
# 添加收藏
db.execute(
favorites.insert().values(user_id=current_user.id, post_id=post.id)
)
db.commit()
from fastapi.responses import RedirectResponse
return RedirectResponse(url=request.headers.get("referer", "/"), status_code=303)
@router.get("/", name="favorites_list")
def favorites_list(
request: Request,
db: Session = Depends(get_db),
current_user = Depends(get_current_user)
):
"""我的收藏列表"""
faved_posts = db.query(Post).join(
favorites, (favorites.c.post_id == Post.id)
).filter(
favorites.c.user_id == current_user.id
).order_by(favorites.c.created_at.desc()).all()
return templates.TemplateResponse("favorites.html", {
"request": request,
"posts": faved_posts
})
在详情页添加收藏按钮
实例
<!-- 文件路径:templates/post_detail.html 标题下方 -->
<div class="post-header">
<h1>{{ post.title }}</h1>
<form method="post" action="/favorites/toggle/{{ post.id }}">
<button type="submit" class="fav-btn">♡ 收藏</button>
</form>
</div>
<div class="post-header">
<h1>{{ post.title }}</h1>
<form method="post" action="/favorites/toggle/{{ post.id }}">
<button type="submit" class="fav-btn">♡ 收藏</button>
</form>
</div>
sqladmin — 后台管理
sqladmin 是 FastAPI 生态中对标 Django Admin 的后台管理扩展。
它专为 SQLAlchemy 设计,与 FastAPI 无缝集成。
(venv) $ pip install sqladmin
实例
# 文件路径:main.py 中配置 sqladmin
from sqladmin import Admin, ModelView
from database import engine
from models import Post, Category, User
class PostAdmin(ModelView, model=Post):
column_list = [Post.id, Post.title, Post.category, Post.created_at]
column_searchable_list = [Post.title, Post.summary]
column_sortable_list = [Post.created_at, Post.title]
form_columns = [Post.title, Post.slug, Post.summary, Post.content, Post.category_id]
class CategoryAdmin(ModelView, model=Category):
column_list = [Category.id, Category.name, Category.slug]
form_columns = [Category.name, Category.slug]
class UserAdmin(ModelView, model=User):
column_list = [User.id, User.username, User.email]
column_searchable_list = [User.username, User.email]
# 不显示密码字段(安全考虑)
form_excluded_columns = [User.hashed_password]
# 创建 Admin 实例,绑定到 FastAPI app
admin = Admin(app, engine, title="RUNOOB 博客后台")
admin.add_view(PostAdmin)
admin.add_view(CategoryAdmin)
admin.add_view(UserAdmin)
from sqladmin import Admin, ModelView
from database import engine
from models import Post, Category, User
class PostAdmin(ModelView, model=Post):
column_list = [Post.id, Post.title, Post.category, Post.created_at]
column_searchable_list = [Post.title, Post.summary]
column_sortable_list = [Post.created_at, Post.title]
form_columns = [Post.title, Post.slug, Post.summary, Post.content, Post.category_id]
class CategoryAdmin(ModelView, model=Category):
column_list = [Category.id, Category.name, Category.slug]
form_columns = [Category.name, Category.slug]
class UserAdmin(ModelView, model=User):
column_list = [User.id, User.username, User.email]
column_searchable_list = [User.username, User.email]
# 不显示密码字段(安全考虑)
form_excluded_columns = [User.hashed_password]
# 创建 Admin 实例,绑定到 FastAPI app
admin = Admin(app, engine, title="RUNOOB 博客后台")
admin.add_view(PostAdmin)
admin.add_view(CategoryAdmin)
admin.add_view(UserAdmin)
访问 /admin 进入后台管理界面。
sqladmin vs Flask-Admin vs Django Admin
| 特性 | Django Admin | Flask-Admin | sqladmin |
|---|---|---|---|
| 注册方式 | admin.site.register() | admin.add_view(ModelView()) | Admin + add_view |
| ORM 支持 | Django ORM | SQLAlchemy / MongoEngine | SQLAlchemy(仅) |
| 认证支持 | 内置 | 需手动集成 | 内置 authentication_backend |
| 异步支持 | 不支持 | 不支持 | 原生 async |
sqladmin 默认不需要登录就能访问。生产环境需要配置
authentication_backend来添加认证保护,详见 sqladmin 官方文档。
本章小结
本章你实现了两个重要功能:用 Table 关联表实现收藏的多对多关系;用 sqladmin 快速搭建后台管理系统。
现在博客的用户交互(注册、登录、收藏)和管理功能(Admin 后台)都已完备。
