# 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. 从查询参数中提取输入
page = request.args.get("page", 1, type=int)
per_page = request.args.get("per_page", 20, type=int)
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. 按时间倒序,然后分页(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, "名称": 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, # 是否有上一页
}
})
# ------------------------------
# 获取单篇文章详情(阅读量自动 +1)
# ------------------------------
@api_bp.route("/articles/<int:article_id>", methods=["GET"])
def get_article(article_id):
# 1. 查不到直接返回 404
article = Article.query.get_or_404(article_id)
# 2. 业务逻辑:每次查看详情,阅读数加 1
article.views += 1
db.session.commit()
# 3. 返回详细信息(包含 Markdown 渲染后的 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():
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):
article = Article.query.get_or_404(article_id)
# 权限检查:只有作者本人或管理员能修改
if article.author_id != current_user.id and not current_user.is_admin:
abort(403, description="无权修改此文章")
data = request.get_json()
if not data:
abort(400, description="请求体不能为空")
# 允许更新的字段白名单(用部分更新兼容 PUT / PATCH)
allowed_fields = ["title", "content", "summary", "is_published", "category_id"]
for field in allowed_fields:
if field in data:
setattr(article, field, data[field])
db.session.commit()
return jsonify({"message": "文章更新成功"}), 200
# ------------------------------
# 删除文章(需要登录且有权限)
# ------------------------------
@api_bp.route("/articles/<int:article_id>", methods=["DELETE"])
@login_required
def delete_article(article_id):
article = Article.query.get_or_404(article_id)
if article.author_id != current_user.id and not current_user.is_admin:
abort(403)
db.session.delete(article)
db.session.commit()
# 删除成功必须返回 204,且不能带响应体
return "", 204