功能特性: - 前端文章展示 (Markdown渲染、代码高亮) - 后台管理系统 (文章、分类、标签、作者管理) - 图片上传 (点击、拖拽、粘贴) - 网站设置 (Logo、底部宣传图片上传) - 访问统计 (IP记录、趋势图表) - 参考来源、附件支持 - 单端口部署 (16012) 技术栈: - Flask 3.0 + SQLAlchemy - SQLite数据库 - marked.js + highlight.js (Markdown渲染) - Chart.js (统计图表)
673 lines
22 KiB
Python
673 lines
22 KiB
Python
"""后台管理路由"""
|
|
from flask import Blueprint, render_template, redirect, url_for, request, flash, session, g, jsonify, send_from_directory
|
|
from functools import wraps
|
|
import os
|
|
import json
|
|
from app.services import ArticleService, CategoryService, TagService, AdminService, SiteConfigService, VisitService, UploadService, AuthorService, ReferenceService, AttachmentService
|
|
|
|
admin_bp = Blueprint('admin', __name__)
|
|
|
|
|
|
def login_required(f):
|
|
"""登录验证装饰器"""
|
|
@wraps(f)
|
|
def decorated_function(*args, **kwargs):
|
|
if 'admin_id' not in session:
|
|
flash('请先登录', 'warning')
|
|
return redirect(url_for('admin.login'))
|
|
return f(*args, **kwargs)
|
|
return decorated_function
|
|
|
|
|
|
# ==================== 登录登出 ====================
|
|
|
|
@admin_bp.route('/login', methods=['GET', 'POST'])
|
|
def login():
|
|
"""登录页面"""
|
|
if request.method == 'POST':
|
|
username = request.form.get('username')
|
|
password = request.form.get('password')
|
|
|
|
admin = AdminService.authenticate(username, password)
|
|
if admin:
|
|
session['admin_id'] = admin.id
|
|
session['admin_username'] = admin.username
|
|
flash('登录成功', 'success')
|
|
return redirect(url_for('admin.dashboard'))
|
|
else:
|
|
flash('用户名或密码错误', 'danger')
|
|
|
|
return render_template('admin/login.html')
|
|
|
|
|
|
@admin_bp.route('/logout')
|
|
def logout():
|
|
"""登出"""
|
|
session.clear()
|
|
flash('已退出登录', 'info')
|
|
return redirect(url_for('admin.login'))
|
|
|
|
|
|
# ==================== 仪表盘 ====================
|
|
|
|
@admin_bp.route('/')
|
|
@admin_bp.route('/dashboard')
|
|
@login_required
|
|
def dashboard():
|
|
"""仪表盘"""
|
|
# 获取统计数据
|
|
articles = ArticleService.get_all_list(per_page=5)
|
|
categories = CategoryService.get_all()
|
|
tags = TagService.get_all()
|
|
|
|
stats = {
|
|
'total_articles': len(articles.items),
|
|
'published_articles': sum(1 for a in articles.items if a.is_published),
|
|
'total_categories': len(categories),
|
|
'total_tags': len(tags),
|
|
'total_views': sum(a.view_count for a in articles.items)
|
|
}
|
|
|
|
return render_template('admin/dashboard.html', stats=stats, recent_articles=articles)
|
|
|
|
|
|
# ==================== 文章管理 ====================
|
|
|
|
@admin_bp.route('/articles')
|
|
@login_required
|
|
def articles():
|
|
"""文章列表"""
|
|
page = request.args.get('page', 1, type=int)
|
|
is_published = request.args.get('status')
|
|
|
|
if is_published == 'published':
|
|
is_published_filter = True
|
|
elif is_published == 'draft':
|
|
is_published_filter = False
|
|
else:
|
|
is_published_filter = None
|
|
|
|
articles = ArticleService.get_all_list(
|
|
page=page,
|
|
per_page=20,
|
|
is_published=is_published_filter
|
|
)
|
|
|
|
categories = CategoryService.get_all()
|
|
tags = TagService.get_all()
|
|
|
|
return render_template('admin/articles.html',
|
|
articles=articles,
|
|
categories=categories,
|
|
tags=tags,
|
|
current_status=is_published)
|
|
|
|
|
|
@admin_bp.route('/articles/create', methods=['GET', 'POST'])
|
|
@login_required
|
|
def article_create():
|
|
"""创建文章"""
|
|
if request.method == 'POST':
|
|
title = request.form.get('title')
|
|
content = request.form.get('content')
|
|
summary = request.form.get('summary')
|
|
author_id = request.form.get('author_id', type=int)
|
|
category_id = request.form.get('category_id', type=int)
|
|
cover_image = request.form.get('cover_image')
|
|
is_published = request.form.get('is_published') == 'on'
|
|
tag_ids = request.form.getlist('tags')
|
|
tag_ids = [int(t) for t in tag_ids if t]
|
|
|
|
# 处理参考来源
|
|
references_json = request.form.get('references', '[]')
|
|
references_data = json.loads(references_json) if references_json else []
|
|
|
|
article = ArticleService.create(
|
|
title=title,
|
|
content=content,
|
|
summary=summary,
|
|
author_id=author_id,
|
|
category_id=category_id,
|
|
tag_ids=tag_ids,
|
|
cover_image=cover_image,
|
|
is_published=is_published
|
|
)
|
|
|
|
# 添加参考来源
|
|
for ref in references_data:
|
|
if ref.get('title'):
|
|
ReferenceService.add(article.id, ref['title'], ref.get('url'), ref.get('description'))
|
|
|
|
flash('文章创建成功', 'success')
|
|
return redirect(url_for('admin.article_edit', article_id=article.id))
|
|
|
|
categories = CategoryService.get_all()
|
|
tags = TagService.get_all()
|
|
authors = AuthorService.get_all()
|
|
return render_template('admin/article_form.html',
|
|
article=None,
|
|
categories=categories,
|
|
tags=tags,
|
|
authors=authors,
|
|
references=[],
|
|
attachments=[])
|
|
|
|
|
|
@admin_bp.route('/articles/<int:article_id>/edit', methods=['GET', 'POST'])
|
|
@login_required
|
|
def article_edit(article_id):
|
|
"""编辑文章"""
|
|
article = ArticleService.get_by_id(article_id)
|
|
if not article:
|
|
flash('文章不存在', 'danger')
|
|
return redirect(url_for('admin.articles'))
|
|
|
|
if request.method == 'POST':
|
|
title = request.form.get('title')
|
|
content = request.form.get('content')
|
|
summary = request.form.get('summary')
|
|
author_id = request.form.get('author_id', type=int)
|
|
category_id = request.form.get('category_id', type=int)
|
|
cover_image = request.form.get('cover_image')
|
|
is_published = request.form.get('is_published') == 'on'
|
|
tag_ids = request.form.getlist('tags')
|
|
tag_ids = [int(t) for t in tag_ids if t]
|
|
|
|
# 处理参考来源
|
|
references_json = request.form.get('references', '[]')
|
|
references_data = json.loads(references_json) if references_json else []
|
|
|
|
ArticleService.update(
|
|
article_id,
|
|
title=title,
|
|
content=content,
|
|
summary=summary,
|
|
author_id=author_id,
|
|
category_id=category_id,
|
|
cover_image=cover_image,
|
|
is_published=is_published,
|
|
tag_ids=tag_ids
|
|
)
|
|
|
|
# 更新参考来源
|
|
ReferenceService.update_article_references(article_id, references_data)
|
|
|
|
flash('文章更新成功', 'success')
|
|
return redirect(url_for('admin.article_edit', article_id=article_id))
|
|
|
|
categories = CategoryService.get_all()
|
|
tags = TagService.get_all()
|
|
authors = AuthorService.get_all()
|
|
references = ReferenceService.get_by_article(article_id)
|
|
attachments = AttachmentService.get_by_article(article_id)
|
|
return render_template('admin/article_form.html',
|
|
article=article,
|
|
categories=categories,
|
|
tags=tags,
|
|
authors=authors,
|
|
references=references,
|
|
attachments=attachments)
|
|
|
|
|
|
@admin_bp.route('/articles/<int:article_id>/delete', methods=['POST'])
|
|
@login_required
|
|
def article_delete(article_id):
|
|
"""删除文章"""
|
|
if ArticleService.delete(article_id):
|
|
flash('文章已删除', 'success')
|
|
else:
|
|
flash('删除失败', 'danger')
|
|
return redirect(url_for('admin.articles'))
|
|
|
|
|
|
@admin_bp.route('/articles/<int:article_id>/toggle-publish', methods=['POST'])
|
|
@login_required
|
|
def article_toggle_publish(article_id):
|
|
"""切换文章发布状态"""
|
|
article = ArticleService.get_by_id(article_id)
|
|
if article:
|
|
ArticleService.update(article_id, is_published=not article.is_published)
|
|
status = '发布' if not article.is_published else '取消发布'
|
|
flash(f'文章已{status}', 'success')
|
|
else:
|
|
flash('文章不存在', 'danger')
|
|
return redirect(url_for('admin.articles'))
|
|
|
|
|
|
# ==================== 分类管理 ====================
|
|
|
|
@admin_bp.route('/categories')
|
|
@login_required
|
|
def categories():
|
|
"""分类列表"""
|
|
categories = CategoryService.get_all()
|
|
return render_template('admin/categories.html', categories=categories)
|
|
|
|
|
|
@admin_bp.route('/categories/create', methods=['GET', 'POST'])
|
|
@login_required
|
|
def category_create():
|
|
"""创建分类"""
|
|
if request.method == 'POST':
|
|
name = request.form.get('name')
|
|
description = request.form.get('description')
|
|
sort_order = request.form.get('sort_order', 0, type=int)
|
|
|
|
CategoryService.create(name=name, description=description, sort_order=sort_order)
|
|
flash('分类创建成功', 'success')
|
|
return redirect(url_for('admin.categories'))
|
|
|
|
return render_template('admin/category_form.html', category=None)
|
|
|
|
|
|
@admin_bp.route('/categories/<int:category_id>/edit', methods=['GET', 'POST'])
|
|
@login_required
|
|
def category_edit(category_id):
|
|
"""编辑分类"""
|
|
category = CategoryService.get_by_id(category_id)
|
|
if not category:
|
|
flash('分类不存在', 'danger')
|
|
return redirect(url_for('admin.categories'))
|
|
|
|
if request.method == 'POST':
|
|
name = request.form.get('name')
|
|
description = request.form.get('description')
|
|
sort_order = request.form.get('sort_order', 0, type=int)
|
|
|
|
CategoryService.update(category_id, name=name, description=description, sort_order=sort_order)
|
|
flash('分类更新成功', 'success')
|
|
return redirect(url_for('admin.categories'))
|
|
|
|
return render_template('admin/category_form.html', category=category)
|
|
|
|
|
|
@admin_bp.route('/categories/<int:category_id>/delete', methods=['POST'])
|
|
@login_required
|
|
def category_delete(category_id):
|
|
"""删除分类"""
|
|
if CategoryService.delete(category_id):
|
|
flash('分类已删除', 'success')
|
|
else:
|
|
flash('删除失败', 'danger')
|
|
return redirect(url_for('admin.categories'))
|
|
|
|
|
|
# ==================== 标签管理 ====================
|
|
|
|
@admin_bp.route('/tags')
|
|
@login_required
|
|
def tags():
|
|
"""标签列表"""
|
|
tags = TagService.get_all()
|
|
return render_template('admin/tags.html', tags=tags)
|
|
|
|
|
|
@admin_bp.route('/tags/create', methods=['GET', 'POST'])
|
|
@login_required
|
|
def tag_create():
|
|
"""创建标签"""
|
|
if request.method == 'POST':
|
|
name = request.form.get('name')
|
|
color = request.form.get('color', '#007bff')
|
|
|
|
TagService.create(name=name, color=color)
|
|
flash('标签创建成功', 'success')
|
|
return redirect(url_for('admin.tags'))
|
|
|
|
return render_template('admin/tag_form.html', tag=None)
|
|
|
|
|
|
@admin_bp.route('/tags/<int:tag_id>/edit', methods=['GET', 'POST'])
|
|
@login_required
|
|
def tag_edit(tag_id):
|
|
"""编辑标签"""
|
|
tag = TagService.get_by_id(tag_id)
|
|
if not tag:
|
|
flash('标签不存在', 'danger')
|
|
return redirect(url_for('admin.tags'))
|
|
|
|
if request.method == 'POST':
|
|
name = request.form.get('name')
|
|
color = request.form.get('color')
|
|
|
|
TagService.update(tag_id, name=name, color=color)
|
|
flash('标签更新成功', 'success')
|
|
return redirect(url_for('admin.tags'))
|
|
|
|
return render_template('admin/tag_form.html', tag=tag)
|
|
|
|
|
|
@admin_bp.route('/tags/<int:tag_id>/delete', methods=['POST'])
|
|
@login_required
|
|
def tag_delete(tag_id):
|
|
"""删除标签"""
|
|
if TagService.delete(tag_id):
|
|
flash('标签已删除', 'success')
|
|
else:
|
|
flash('删除失败', 'danger')
|
|
return redirect(url_for('admin.tags'))
|
|
|
|
|
|
# ==================== 网站设置 ====================
|
|
|
|
@admin_bp.route('/settings', methods=['GET', 'POST'])
|
|
@login_required
|
|
def settings():
|
|
"""网站设置"""
|
|
if request.method == 'POST':
|
|
# 处理图片上传
|
|
promo_image_1 = request.form.get('promo_image_1', '')
|
|
promo_image_2 = request.form.get('promo_image_2', '')
|
|
promo_image_3 = request.form.get('promo_image_3', '')
|
|
|
|
# 上传图片1
|
|
if 'promo_image_1_file' in request.files:
|
|
file = request.files['promo_image_1_file']
|
|
if file and file.filename:
|
|
image, error = UploadService.upload_image(file)
|
|
if image:
|
|
promo_image_1 = UploadService.get_image_url(image.filename)
|
|
|
|
# 上传图片2
|
|
if 'promo_image_2_file' in request.files:
|
|
file = request.files['promo_image_2_file']
|
|
if file and file.filename:
|
|
image, error = UploadService.upload_image(file)
|
|
if image:
|
|
promo_image_2 = UploadService.get_image_url(image.filename)
|
|
|
|
# 上传图片3
|
|
if 'promo_image_3_file' in request.files:
|
|
file = request.files['promo_image_3_file']
|
|
if file and file.filename:
|
|
image, error = UploadService.upload_image(file)
|
|
if image:
|
|
promo_image_3 = UploadService.get_image_url(image.filename)
|
|
|
|
# 更新设置
|
|
SiteConfigService.update(
|
|
site_name=request.form.get('site_name', '技术博客'),
|
|
site_subtitle=request.form.get('site_subtitle', ''),
|
|
site_logo=request.form.get('site_logo', ''),
|
|
site_keywords=request.form.get('site_keywords', ''),
|
|
site_description=request.form.get('site_description', ''),
|
|
footer_text=request.form.get('footer_text', ''),
|
|
footer_copyright=request.form.get('footer_copyright', ''),
|
|
footer_technology=request.form.get('footer_technology', ''),
|
|
promo_image_1=promo_image_1,
|
|
promo_image_1_title=request.form.get('promo_image_1_title', ''),
|
|
promo_image_1_link=request.form.get('promo_image_1_link', ''),
|
|
promo_image_2=promo_image_2,
|
|
promo_image_2_title=request.form.get('promo_image_2_title', ''),
|
|
promo_image_2_link=request.form.get('promo_image_2_link', ''),
|
|
promo_image_3=promo_image_3,
|
|
promo_image_3_title=request.form.get('promo_image_3_title', ''),
|
|
contact_email=request.form.get('contact_email', ''),
|
|
contact_github=request.form.get('contact_github', ''),
|
|
articles_per_page=request.form.get('articles_per_page', 10, type=int),
|
|
)
|
|
flash('网站设置已更新', 'success')
|
|
return redirect(url_for('admin.settings'))
|
|
|
|
config = SiteConfigService.get_config()
|
|
return render_template('admin/settings.html', config=config)
|
|
|
|
|
|
# ==================== 访问统计 ====================
|
|
|
|
@admin_bp.route('/stats')
|
|
@login_required
|
|
def stats():
|
|
"""访问统计概览"""
|
|
stats_summary = VisitService.get_stats_summary()
|
|
top_ips = VisitService.get_top_ips(10)
|
|
top_articles = VisitService.get_top_articles(7, 10)
|
|
top_paths = VisitService.get_top_paths(20)
|
|
|
|
return render_template('admin/stats.html',
|
|
stats=stats_summary,
|
|
top_ips=top_ips,
|
|
top_articles=top_articles,
|
|
top_paths=top_paths)
|
|
|
|
|
|
@admin_bp.route('/stats/logs')
|
|
@login_required
|
|
def stats_logs():
|
|
"""访问日志列表"""
|
|
page = request.args.get('page', 1, type=int)
|
|
path_filter = request.args.get('path', '')
|
|
|
|
logs = VisitService.get_visit_list(page=page, per_page=50, path_filter=path_filter)
|
|
|
|
return render_template('admin/stats_logs.html',
|
|
logs=logs,
|
|
path_filter=path_filter)
|
|
|
|
|
|
@admin_bp.route('/stats/charts')
|
|
@login_required
|
|
def stats_charts():
|
|
"""访问统计图表"""
|
|
stats_summary = VisitService.get_stats_summary()
|
|
|
|
# 准备图表数据
|
|
daily_data = []
|
|
for stat in stats_summary['daily_stats']:
|
|
daily_data.append({
|
|
'date': stat.date.strftime('%Y-%m-%d'),
|
|
'visits': stat.total_visits,
|
|
'unique_ips': stat.unique_ips,
|
|
'article_views': stat.article_views
|
|
})
|
|
|
|
return render_template('admin/stats_charts.html',
|
|
daily_data=daily_data,
|
|
stats=stats_summary)
|
|
|
|
|
|
@admin_bp.route('/stats/clear', methods=['POST'])
|
|
@login_required
|
|
def stats_clear():
|
|
"""清理旧访问日志"""
|
|
days = request.form.get('days', 90, type=int)
|
|
deleted = VisitService.clear_old_logs(days)
|
|
flash(f'已清理 {deleted} 条旧日志记录', 'success')
|
|
return redirect(url_for('admin.stats_logs'))
|
|
|
|
|
|
# ==================== 图片上传 ====================
|
|
|
|
@admin_bp.route('/upload/image', methods=['POST'])
|
|
@login_required
|
|
def upload_image():
|
|
"""上传图片(支持拖拽、粘贴、选择)"""
|
|
from flask import current_app
|
|
|
|
article_id = request.form.get('article_id', type=int)
|
|
uploader = session.get('admin_username', 'admin')
|
|
|
|
# 检查是否有文件上传
|
|
if 'file' not in request.files:
|
|
return jsonify({'success': False, 'message': '没有选择文件'}), 400
|
|
|
|
file = request.files['file']
|
|
if file.filename == '':
|
|
return jsonify({'success': False, 'message': '没有选择文件'}), 400
|
|
|
|
# 上传图片
|
|
image, error = UploadService.upload_image(file, article_id, uploader)
|
|
if error:
|
|
return jsonify({'success': False, 'message': error}), 400
|
|
|
|
# 返回图片URL
|
|
return jsonify({
|
|
'success': True,
|
|
'url': UploadService.get_image_url(image.filename),
|
|
'filename': image.filename
|
|
})
|
|
|
|
|
|
@admin_bp.route('/upload/paste', methods=['POST'])
|
|
@login_required
|
|
def upload_paste():
|
|
"""粘贴图片上传"""
|
|
from flask import current_app
|
|
import base64
|
|
from io import BytesIO
|
|
|
|
article_id = request.form.get('article_id', type=int)
|
|
uploader = session.get('admin_username', 'admin')
|
|
|
|
# 获取粘贴的图片数据
|
|
image_data = request.form.get('image_data')
|
|
if not image_data:
|
|
return jsonify({'success': False, 'message': '没有图片数据'}), 400
|
|
|
|
# 解码Base64图片
|
|
try:
|
|
# 移除data:image/xxx;base64,前缀
|
|
if 'base64,' in image_data:
|
|
image_data = image_data.split('base64,')[1]
|
|
|
|
image_bytes = base64.b64decode(image_data)
|
|
image_file = BytesIO(image_bytes)
|
|
|
|
# 确定图片格式
|
|
format_map = {'png': 'png', 'jpeg': 'jpg', 'jpg': 'jpg', 'gif': 'gif', 'webpo': 'webpo'}
|
|
# 从数据头识别格式(简化处理)
|
|
ext = 'png' # 默认png
|
|
|
|
# 生成文件名
|
|
from datetime import datetime
|
|
import uuid
|
|
filename = f'{datetime.now().strftime("%Y%m%d")}_{uuid.uuid4().hex[:8]}.{ext}'
|
|
|
|
# 保存文件
|
|
upload_folder = UploadService.get_upload_folder()
|
|
filepath = os.path.join(upload_folder, filename)
|
|
with open(filepath, 'wb') as f:
|
|
f.write(image_bytes)
|
|
|
|
# 记录到数据库
|
|
from app.models import UploadedImage
|
|
image = UploadedImage(
|
|
filename=filename,
|
|
filepath=filepath,
|
|
article_id=article_id,
|
|
uploader=uploader
|
|
)
|
|
db.session.add(image)
|
|
db.session.commit()
|
|
|
|
return jsonify({
|
|
'success': True,
|
|
'url': UploadService.get_image_url(filename),
|
|
'filename': filename
|
|
})
|
|
except Exception as e:
|
|
return jsonify({'success': False, 'message': str(e)}), 400
|
|
|
|
|
|
@admin_bp.route('/upload/attachment', methods=['POST'])
|
|
@login_required
|
|
def upload_attachment():
|
|
"""上传附件"""
|
|
article_id = request.form.get('article_id', type=int)
|
|
description = request.form.get('description', '')
|
|
|
|
if 'file' not in request.files:
|
|
return jsonify({'success': False, 'message': '没有选择文件'}), 400
|
|
|
|
file = request.files['file']
|
|
if file.filename == '':
|
|
return jsonify({'success': False, 'message': '没有选择文件'}), 400
|
|
|
|
attachment, error = UploadService.upload_attachment(file, article_id, description)
|
|
if error:
|
|
return jsonify({'success': False, 'message': error}), 400
|
|
|
|
return jsonify({
|
|
'success': True,
|
|
'attachment': {
|
|
'id': attachment.id,
|
|
'filename': attachment.filename,
|
|
'filesize': attachment.get_filesize_display(),
|
|
'url': f'/uploads/{attachment.filename}'
|
|
}
|
|
})
|
|
|
|
|
|
@admin_bp.route('/attachment/<int:attachment_id>/delete', methods=['POST'])
|
|
@login_required
|
|
def delete_attachment(attachment_id):
|
|
"""删除附件"""
|
|
if AttachmentService.delete_attachment(attachment_id):
|
|
return jsonify({'success': True})
|
|
return jsonify({'success': False, 'message': '删除失败'}), 400
|
|
|
|
|
|
# ==================== 作者管理 ====================
|
|
|
|
@admin_bp.route('/authors')
|
|
@login_required
|
|
def authors():
|
|
"""作者列表"""
|
|
authors = AuthorService.get_all()
|
|
return render_template('admin/authors.html', authors=authors)
|
|
|
|
|
|
@admin_bp.route('/authors/create', methods=['GET', 'POST'])
|
|
@login_required
|
|
def author_create():
|
|
"""创建作者"""
|
|
if request.method == 'POST':
|
|
name = request.form.get('name')
|
|
avatar = request.form.get('avatar')
|
|
bio = request.form.get('bio')
|
|
website = request.form.get('website')
|
|
|
|
AuthorService.create(name=name, avatar=avatar, bio=bio, website=website)
|
|
flash('作者创建成功', 'success')
|
|
return redirect(url_for('admin.authors'))
|
|
|
|
return render_template('admin/author_form.html', author=None)
|
|
|
|
|
|
@admin_bp.route('/authors/<int:author_id>/edit', methods=['GET', 'POST'])
|
|
@login_required
|
|
def author_edit(author_id):
|
|
"""编辑作者"""
|
|
author = AuthorService.get_by_id(author_id)
|
|
if not author:
|
|
flash('作者不存在', 'danger')
|
|
return redirect(url_for('admin.authors'))
|
|
|
|
if request.method == 'POST':
|
|
AuthorService.update(author_id,
|
|
name=request.form.get('name'),
|
|
avatar=request.form.get('avatar'),
|
|
bio=request.form.get('bio'),
|
|
website=request.form.get('website')
|
|
)
|
|
flash('作者更新成功', 'success')
|
|
return redirect(url_for('admin.authors'))
|
|
|
|
return render_template('admin/author_form.html', author=author)
|
|
|
|
|
|
@admin_bp.route('/authors/<int:author_id>/delete', methods=['POST'])
|
|
@login_required
|
|
def author_delete(author_id):
|
|
"""删除作者"""
|
|
if AuthorService.delete(author_id):
|
|
flash('作者已删除', 'success')
|
|
else:
|
|
flash('删除失败', 'danger')
|
|
return redirect(url_for('admin.authors'))
|
|
|
|
|
|
# ==================== 文件访问 ====================
|
|
|
|
# 注意:文件访问路由已在 app/__init__.py 中注册为全局路由
|
|
# 这里不需要重复注册 |