# app/api/articles.py
from flask import jsonify, request, abort
from flask_login import login_required, current_user
from app.api import api_bp
from app.extensions import db
from app.models import Article, Category
from app.forms import ArticleForm
from app.utils import render_markdown
# ------------------------------
# 获取文章列表(分页+分类筛选)
# ------------------------------
@api_bp.route("/articles", methods=["GET"])
def list_articles():
# 1. 从URL查询参数获取输入
page = request.args.get("page", 1, type=int) # 默认第1页
per_page = request.args.get("per_page", 20, type=int) # 默认每页20条
category_id = request.args.get("category", type=int) # 可选分类筛选
# 2. 构建查询语句:只返回已发布的文章
query = Article.query.filter_by(is_published=True)
if category_id:
query = query.filter_by(category_id=category_id)
# 3. 使用SQLAlchemy的paginate实现分页(error_out=False时,越界返回空列表而非404)
pagination = query.order_by(Article.created_at.desc())\
.paginate(page=page, per_page=per_page, error_out=False)
# 4. 组装返回的JSON数据
return jsonify({
"data": [
{
"id": a.id,
"title": a.title,
"summary": a.summary,
"author": {"id": a.author.id, "name": a.author.username},
"views": a.views,
"created_at": a.created_at.isoformat(), # 统一用ISO8601时间格式
}
for a in pagination.items
],
"pagination": {
"total": pagination.total, # 总文章数
"page": page, # 当前页
"pages": pagination.pages, # 总页数
"has_next": pagination.has_next, # 是否有下一页
"has_prev": pagination.has_prev, # 是否有上一页
}
})
# ------------------------------
# 获取单篇文章详情(自动增加阅读量)
# ------------------------------
@api_bp.route("/articles/<int:article_id>", methods=["GET"])
def get_article(article_id):
# 1. 查找文章,找不到直接返回404(Flask的get_or_404会自动处理)
article = Article.query.get_or_404(article_id)
# 2. 业务逻辑:每次打开详情页阅读量+1
article.views += 1
db.session.commit()
# 3. 返回更详细的信息(包含完整内容、HTML渲染后的内容、作者头像、分类等)
return jsonify({
"id": article.id,
"title": article.title,
"content": article.content,
"content_html": render_markdown(article.content), # 把Markdown转成HTML返回给前端
"summary": article.summary,
"author": {
"id": article.author.id,
"name": article.author.username,
"avatar": article.author.get_avatar(),
},
"category": {"id": article.category.id, "name": article.category.name} if article.category else None,
"views": article.views,
"created_at": article.created_at.isoformat(),
"updated_at": article.updated_at.isoformat(),
})
# ------------------------------
# 创建新文章(需要登录)
# ------------------------------
@api_bp.route("/articles", methods=["POST"])
@login_required # Flask-Login装饰器,验证用户是否已登录
def create_article():
# 1. 检查请求体是否为有效的JSON
data = request.get_json()
if not data:
abort(400, description="请求体必须是有效的JSON格式")
# 2. 使用表单验证(Flask-WTF)或Marshmallow验证数据
form = ArticleForm(data=data)
if not form.validate():
# 验证失败返回422和具体错误
return jsonify({"error": "数据验证失败", "details": form.errors}), 422
# 3. 组装数据并保存到数据库
article = Article(
title=form.title.data,
content=form.content.data,
summary=form.summary.data,
category_id=form.category_id.data or None,
author=current_user,
)
db.session.add(article)
db.session.commit()
# 4. 返回201状态码和新建文章的ID
return jsonify({"id": article.id, "message": "文章创建成功"}), 201
# ------------------------------
# 全量/部分更新文章(需要登录且有权限)
# ------------------------------
@api_bp.route("/articles/<int:article_id>", methods=["PUT"])
@login_required
def update_article(article_id):
# 1. 查找文章
article = Article.query.get_or_404(article_id)
# 2. 权限验证:只有作者本人或管理员能修改
if article.author_id != current_user.id and not current_user.is_admin:
abort(403, description="无权修改此文章")
# 3. 获取请求JSON
data = request.get_json()
if not data:
abort(400, description="请求体不能为空")
# 4. 只更新请求中存在的字段(部分更新用PATCH,不过PUT这里也兼容了)
allowed_fields = ["title", "content", "summary", "is_published", "category_id"]
for field in allowed_fields:
if field in data:
setattr(article, field, data[field])
# 5. 保存并返回
db.session.commit()
return jsonify({"message": "文章更新成功"}), 200
# ------------------------------
# 删除文章(需要登录且有权限)
# ------------------------------
@api_bp.route("/articles/<int:article_id>", methods=["DELETE"])
@login_required
def delete_article(article_id):
# 1. 查找文章
article = Article.query.get_or_404(article_id)
# 2. 权限验证
if article.author_id != current_user.id and not current_user.is_admin:
abort(403)
# 3. 删除并保存
db.session.delete(article)
db.session.commit()
# 4. 删除成功必须返回204状态码,且不能有返回体
return "", 204