From 503a9780b261b172aae76541b4cf964e259f8dd0 Mon Sep 17 00:00:00 2001 From: hz4th_coder Date: Thu, 25 Jun 2026 11:21:36 +0800 Subject: [PATCH] =?UTF-8?q?refactor:=20=E6=8B=86=E5=88=86admin=E8=B7=AF?= =?UTF-8?q?=E7=94=B1=E4=B8=BA=E6=A8=A1=E5=9D=97=E5=8C=96=E7=BB=93=E6=9E=84?= =?UTF-8?q?=EF=BC=8C=E7=AB=AF=E5=8F=A3=E6=94=B9=E4=B8=BA16013?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 将 routes/admin.py (672行) 拆分为 routes/admin/ 包 - 按功能模块拆分:auth, dashboard, articles, categories, tags, settings, stats, upload, authors - 端口从 16012 改为 16013 - 初始化数据库和管理员账户 --- app/routes/admin.py | 673 --------------------------------- app/routes/admin/__init__.py | 20 + app/routes/admin/articles.py | 166 ++++++++ app/routes/admin/auth.py | 31 ++ app/routes/admin/authors.py | 62 +++ app/routes/admin/categories.py | 60 +++ app/routes/admin/dashboard.py | 24 ++ app/routes/admin/settings.py | 67 ++++ app/routes/admin/stats.py | 65 ++++ app/routes/admin/tags.py | 58 +++ app/routes/admin/upload.py | 129 +++++++ run.py | 2 +- 12 files changed, 683 insertions(+), 674 deletions(-) delete mode 100644 app/routes/admin.py create mode 100644 app/routes/admin/__init__.py create mode 100644 app/routes/admin/articles.py create mode 100644 app/routes/admin/auth.py create mode 100644 app/routes/admin/authors.py create mode 100644 app/routes/admin/categories.py create mode 100644 app/routes/admin/dashboard.py create mode 100644 app/routes/admin/settings.py create mode 100644 app/routes/admin/stats.py create mode 100644 app/routes/admin/tags.py create mode 100644 app/routes/admin/upload.py diff --git a/app/routes/admin.py b/app/routes/admin.py deleted file mode 100644 index dd9edf1..0000000 --- a/app/routes/admin.py +++ /dev/null @@ -1,673 +0,0 @@ -"""后台管理路由""" -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//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//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//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//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//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//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//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//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//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//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 中注册为全局路由 -# 这里不需要重复注册 \ No newline at end of file diff --git a/app/routes/admin/__init__.py b/app/routes/admin/__init__.py new file mode 100644 index 0000000..3784159 --- /dev/null +++ b/app/routes/admin/__init__.py @@ -0,0 +1,20 @@ +"""后台管理路由模块""" +from flask import Blueprint, session, flash, redirect, url_for +from functools import wraps + +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 + + +# 导入子模块以注册路由 +from app.routes.admin import auth, dashboard, articles, categories, tags, settings, stats, upload, authors diff --git a/app/routes/admin/articles.py b/app/routes/admin/articles.py new file mode 100644 index 0000000..5032096 --- /dev/null +++ b/app/routes/admin/articles.py @@ -0,0 +1,166 @@ +"""文章管理路由""" +from flask import render_template, redirect, url_for, request, flash +import json +from app.routes.admin import admin_bp, login_required +from app.services import ArticleService, CategoryService, TagService, AuthorService, ReferenceService, AttachmentService + + +@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//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//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//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')) diff --git a/app/routes/admin/auth.py b/app/routes/admin/auth.py new file mode 100644 index 0000000..f289726 --- /dev/null +++ b/app/routes/admin/auth.py @@ -0,0 +1,31 @@ +"""认证相关路由:登录/登出""" +from flask import render_template, redirect, url_for, request, flash, session +from app.routes.admin import admin_bp +from app.services import AdminService + + +@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')) diff --git a/app/routes/admin/authors.py b/app/routes/admin/authors.py new file mode 100644 index 0000000..3480429 --- /dev/null +++ b/app/routes/admin/authors.py @@ -0,0 +1,62 @@ +"""作者管理路由""" +from flask import render_template, redirect, url_for, request, flash +from app.routes.admin import admin_bp, login_required +from app.services import AuthorService + + +@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//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//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')) diff --git a/app/routes/admin/categories.py b/app/routes/admin/categories.py new file mode 100644 index 0000000..7b945e6 --- /dev/null +++ b/app/routes/admin/categories.py @@ -0,0 +1,60 @@ +"""分类管理路由""" +from flask import render_template, redirect, url_for, request, flash +from app.routes.admin import admin_bp, login_required +from app.services import CategoryService + + +@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//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//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')) diff --git a/app/routes/admin/dashboard.py b/app/routes/admin/dashboard.py new file mode 100644 index 0000000..0a40bfc --- /dev/null +++ b/app/routes/admin/dashboard.py @@ -0,0 +1,24 @@ +"""仪表盘路由""" +from flask import render_template +from app.routes.admin import admin_bp, login_required +from app.services import ArticleService, CategoryService, TagService + + +@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) diff --git a/app/routes/admin/settings.py b/app/routes/admin/settings.py new file mode 100644 index 0000000..f4e1eba --- /dev/null +++ b/app/routes/admin/settings.py @@ -0,0 +1,67 @@ +"""网站设置路由""" +from flask import render_template, redirect, url_for, request, flash +from app.routes.admin import admin_bp, login_required +from app.services import SiteConfigService, UploadService + + +@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) diff --git a/app/routes/admin/stats.py b/app/routes/admin/stats.py new file mode 100644 index 0000000..d620caa --- /dev/null +++ b/app/routes/admin/stats.py @@ -0,0 +1,65 @@ +"""访问统计路由""" +from flask import render_template, redirect, url_for, request, flash +from app.routes.admin import admin_bp, login_required +from app.services import VisitService + + +@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')) diff --git a/app/routes/admin/tags.py b/app/routes/admin/tags.py new file mode 100644 index 0000000..2d4470d --- /dev/null +++ b/app/routes/admin/tags.py @@ -0,0 +1,58 @@ +"""标签管理路由""" +from flask import render_template, redirect, url_for, request, flash +from app.routes.admin import admin_bp, login_required +from app.services import TagService + + +@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//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//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')) diff --git a/app/routes/admin/upload.py b/app/routes/admin/upload.py new file mode 100644 index 0000000..c797c69 --- /dev/null +++ b/app/routes/admin/upload.py @@ -0,0 +1,129 @@ +"""文件上传路由:图片/附件""" +from flask import render_template, redirect, url_for, request, flash, session, jsonify +import os +from app.routes.admin import admin_bp, login_required +from app.services import UploadService, AttachmentService + + +@admin_bp.route('/upload/image', methods=['POST']) +@login_required +def upload_image(): + """上传图片(支持拖拽、粘贴、选择)""" + 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(): + """粘贴图片上传""" + import base64 + from io import BytesIO + from datetime import datetime + import uuid + + 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) + + # 确定图片格式 + ext = 'png' # 默认png + + # 生成文件名 + 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 + from app import db + 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//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 diff --git a/run.py b/run.py index 68817ab..62c905d 100644 --- a/run.py +++ b/run.py @@ -7,5 +7,5 @@ app = create_app(os.environ.get('FLASK_ENV', 'development')) if __name__ == '__main__': # 从环境变量获取端口,默认16012 - port = int(os.environ.get('PORT', 16012)) + port = int(os.environ.get('PORT', 16013)) app.run(host='0.0.0.0', port=port) \ No newline at end of file