Python基于 Gradio 和 SQLite 开发的简单博客管理平台,支持局域网手机查看,给一个PC和手机 互联方式

图:http://localhost:7860

🌟 功能特性

📚 核心功能

  • 文章管理: 创建、编辑、删除博客文章

  • 分类系统: 支持文章分类和标签管理

  • 搜索功能: 全文搜索,支持按标题或内容搜索

  • 浏览统计: 自动记录文章浏览次数

  • 附件管理: 文件上传、下载、删除功能

  • 响应式界面: 基于Gradio的现代化Web界面

🔍 搜索和筛选

  • 🔎 关键词搜索: 在标题、内容或全文中搜索

  • 📂 分类筛选: 按文章分类快速筛选

  • 📊 实时更新: 搜索结果实时显示

💾 数据存储

  • 🗄️ SQLite数据库: 轻量级、零配置的数据库

  • 🔒 数据安全: ACID事务保证数据完整性

  • 📈 性能优化: 数据库索引优化查询速度

🚀 快速开始

1. 环境要求

  • Python 3.7+

  • pip 包管理器

2. 安装依赖

复制代码
pip install -r requirements.txt

gradio>=4.0.0

pandas>=1.3.0

qrcode>=7.4.2

3. 运行系统

复制代码
python main.py

4. 访问界面

📖 使用指南

🏠 主界面导航

系统采用标签页设计,包含以下功能模块:

📚 浏览文章
  • 查看所有已发布的文章

  • 使用搜索功能查找特定文章

  • 按分类筛选文章

  • 查看文章详细信息(作者、创建时间、浏览量等)

✍️ 创建文章
  • 填写文章标题和内容

  • 设置作者信息

  • 选择文章分类

  • 添加标签(多个标签用逗号分隔)

  • 发布文章

✏️ 编辑文章
  1. 输入要编辑的文章ID

  2. 点击"加载文章"按钮

  3. 修改文章信息

  4. 点击"更新文章"保存修改

🗑️ 删除文章
  1. 输入要删除的文章ID

  2. 点击"删除文章"按钮

  3. ⚠️ 注意: 删除操作不可恢复

🔍 搜索功能详解

搜索范围选项:
  • all: 在标题和内容中搜索

  • title: 仅在标题中搜索

  • content: 仅在内容中搜索

搜索技巧:
  • 支持部分匹配

  • 不区分大小写

  • 支持中英文混合搜索

📁 项目结构

复制代码
博客管理系统/
├── main.py              # 主程序入口
├── database.py          # 数据库初始化模块
├── blog_manager.py      # 博客数据操作类
├── attachment_manager.py # 附件管理类
├── gradio_interface.py  # Gradio界面组件
├── setup_storage.py     # 文件存储初始化脚本
├── requirements.txt     # 项目依赖
├── blog.db             # SQLite数据库文件(自动生成)
├── uploads/            # 附件存储目录(自动生成)
│   ├── 2024/
│   │   └── 01/         # 按年月组织的文件
│   ├── .gitignore
│   └── README.md
└── README.md           # 项目说明文档

🛠️ 技术架构

核心技术栈

  • 前端界面: Gradio 4.0+

  • 后端逻辑: Python 3.7+

  • 数据库: SQLite 3

  • 数据处理: Pandas

架构设计

复制代码
用户界面 (Gradio)
    ↓
业务逻辑层 (BlogManager)
    ↓
数据访问层 (BlogDatabase) 
    ↓
数据存储 (SQLite)

📊 数据库设计

文章表 (posts)

字段名 类型 说明
id INTEGER 主键,自增ID
title TEXT 文章标题
content TEXT 文章内容
author TEXT 作者姓名
category TEXT 文章分类
tags TEXT 文章标签
created_at TIMESTAMP 创建时间
updated_at TIMESTAMP 更新时间
is_published BOOLEAN 发布状态
view_count INTEGER 浏览次数

索引优化

  • idx_posts_title: 标题索引,优化搜索性能

  • idx_posts_category: 分类索引,优化筛选性能

  • idx_posts_created_at: 时间索引,优化排序性能

🔧 自定义配置

修改端口号

main.py 中修改:

复制代码
interface.launch(
    server_port=7860,  # 修改此处的端口号
    # ... 其他配置
)

修改数据库路径

BlogManager 初始化时指定:

复制代码
blog_manager = BlogManager(db_path="your_custom_path.db")

界面主题定制

gradio_interface.py 中的 custom_css 变量修改样式。

🚨 注意事项

数据安全

  • 定期备份 blog.db 文件

  • 删除操作不可恢复,请谨慎操作

  • 建议在生产环境中使用更强的数据库

性能优化

  • 大量文章时建议添加分页功能

  • 可考虑添加文章缓存机制

  • 定期清理和优化数据库

兼容性

  • 支持Windows、macOS、Linux系统

  • 需要Python 3.7或更高版本

  • 建议使用现代浏览器访问界面

🐛 故障排除

常见问题

1. 端口被占用

复制代码
Error: Port 7860 is already in use

解决方法:修改 main.py 中的端口号或关闭占用端口的程序。

2. 模块导入错误

复制代码
ImportError: No module named 'gradio'

解决方法:运行 pip install -r requirements.txt 安装依赖。

3. 数据库权限错误

复制代码
sqlite3.OperationalError: database is locked

解决方法:确保没有其他程序访问数据库文件,或重启应用。

调试模式

main.py 中启用调试模式:

复制代码
interface.launch(debug=True)

🔮 代码:

main.py

python 复制代码
#!/usr/bin/env python3
"""
博客管理系统主程序
基于 Gradio + SQLite 开发的简单博客管理平台

功能特性:
- 📚 浏览和搜索文章
- ✍️ 创建新文章
- ✏️ 编辑现有文章  
- 🗑️ 删除文章
- 📂 按分类筛选
- 🔍 全文搜索功能
- 📊 文章浏览统计

使用方法:
    python main.py

作者: AI Assistant
版本: 1.0.0
"""

import sys
import os
from pathlib import Path

# 确保当前目录在Python路径中
current_dir = Path(__file__).parent
sys.path.insert(0, str(current_dir))

try:
    from gradio_interface import BlogGradioInterface
    from blog_manager import BlogManager
    from database import BlogDatabase
    from setup_storage import setup_upload_directory, validate_upload_security
except ImportError as e:
    print(f"❌ 导入模块失败: {e}")
    print("请确保所有依赖文件都在同一目录下")
    sys.exit(1)


def initialize_system():
    """初始化系统,创建示例数据"""
    print("🔧 正在初始化博客管理系统...")
    
    try:
        # 初始化数据库
        db = BlogDatabase()
        blog_manager = BlogManager()
        
        # 检查是否已有数据
        posts = blog_manager.get_all_posts(published_only=False)
        
        if not posts:
            print("📝 检测到空数据库,正在创建示例文章...")
            
            # 创建一些示例文章
            sample_posts = [
                {
                    "title": "欢迎使用博客管理系统",
                    "content": """# 欢迎来到我的博客!

这是一个基于 **Gradio** 和 **SQLite** 开发的简单博客管理系统。

## 主要功能

### 📚 文章管理
- 创建、编辑、删除文章
- 支持markdown格式
- 文章分类和标签管理

### 🔍 搜索功能  
- 全文搜索
- 按标题或内容搜索
- 分类筛选

### 📊 统计信息
- 文章浏览量统计
- 创建和更新时间记录

## 如何使用

1. **浏览文章**: 在"浏览文章"标签页查看所有文章
2. **创建文章**: 在"创建文章"标签页写新文章
3. **编辑文章**: 在"编辑文章"标签页修改现有文章
4. **删除文章**: 在"删除文章"标签页删除不需要的文章

享受写作的乐趣吧! ✨""",
                    "author": "系统管理员",
                    "category": "系统",
                    "tags": "欢迎,使用指南,博客"
                },
                {
                    "title": "Gradio 简介",
                    "content": """# Gradio - 快速构建ML界面

Gradio 是一个开源的Python库,让你能够快速为机器学习模型创建易于使用的Web界面。

## 主要特点

- **简单易用**: 几行代码就能创建界面
- **灵活定制**: 支持多种输入输出组件
- **实时交互**: 支持实时预览和交互
- **部署便捷**: 可以轻松分享和部署

## 在博客系统中的应用

本系统使用Gradio构建了用户友好的Web界面,包括:

- 标签页导航
- 表单输入组件
- 文本显示组件
- 按钮交互
- 实时状态更新

这使得用户无需了解复杂的web开发技术,就能享受现代化的用户体验。""",
                    "author": "技术团队",
                    "category": "技术",
                    "tags": "Gradio,Python,Web开发"
                },
                {
                    "title": "SQLite 数据库优势",
                    "content": """# SQLite - 轻量级关系数据库

SQLite是一个嵌入式的关系数据库管理系统,特别适合小型到中型应用。

## 主要优势

### 🚀 零配置
- 无需安装服务器
- 单个文件存储
- 即开即用

### 📱 轻量级
- 体积小巧
- 资源占用少
- 性能优秀

### 🔒 稳定可靠
- ACID事务支持
- 数据完整性保证
- 广泛应用验证

## 在博客系统中的作用

- **文章存储**: 保存博客文章的所有信息
- **索引优化**: 提供快速搜索能力
- **数据安全**: 保证数据一致性和完整性

SQLite的简单性和可靠性使其成为本博客系统的完美选择!""",
                    "author": "数据库专家",
                    "category": "技术", 
                    "tags": "SQLite,数据库,存储"
                }
            ]
            
            for post_data in sample_posts:
                success = blog_manager.create_post(
                    title=post_data['title'],
                    content=post_data['content'],
                    author=post_data['author'],
                    category=post_data['category'],
                    tags=post_data['tags']
                )
                if success:
                    print(f"✅ 创建示例文章: {post_data['title']}")
                else:
                    print(f"❌ 创建示例文章失败: {post_data['title']}")
            
            print("📝 示例文章创建完成!")
        else:
            print(f"📚 发现现有数据库,包含 {len(posts)} 篇文章")
        
        print("✅ 系统初始化完成!")
        
        # 初始化存储目录
        print("\n📁 初始化文件存储系统...")
        storage_ok = setup_upload_directory()
        if storage_ok:
            security_ok = validate_upload_security()
            if security_ok:
                print("✅ 文件存储系统初始化成功!")
            else:
                print("⚠️ 文件存储系统初始化完成,但存在安全问题")
        else:
            print("❌ 文件存储系统初始化失败")
        
        return True
        
    except Exception as e:
        print(f"❌ 系统初始化失败: {e}")
        return False


def main():
    """主函数"""
    print("=" * 60)
    print("🚀 博客管理系统启动中...")
    print("=" * 60)
    
    # 初始化系统
    if not initialize_system():
        print("❌ 系统初始化失败,程序退出")
        sys.exit(1)
    
    try:
        # 创建界面
        print("\n🎨 正在创建用户界面...")
        blog_interface = BlogGradioInterface()
        interface = blog_interface.create_interface()
        
        print("\n" + "=" * 60)
        print("🎉 博客管理系统启动成功!")
        print("=" * 60)
        print("📝 系统功能:")
        print("   • 📚 浏览和搜索文章")
        print("   • 📖 查看文章完整内容")
        print("   • ✍️ 创建新文章")
        print("   • ✏️ 编辑现有文章")
        print("   • 🗑️ 删除文章")
        print("   • 📂 按分类筛选")
        print("   • 🔍 全文搜索")
        print("   • 📁 附件管理(上传、下载、删除)")
        print("\n🌐 访问地址:")
        print("   • 本地访问: http://localhost:7860")
        print("   • 局域网访问: http://0.0.0.0:7860")
        print("\n💡 提示: 按 Ctrl+C 停止服务器")
        print("=" * 60)
        
        # 启动界面
        interface.launch(
            server_name="0.0.0.0",
            server_port=7860,
            share=False,
            debug=False,
            show_error=True,
            quiet=False
        )
        
    except KeyboardInterrupt:
        print("\n\n👋 博客管理系统已停止,感谢使用!")
        sys.exit(0)
    except Exception as e:
        print(f"\n❌ 系统运行出错: {e}")
        print("请检查错误信息并重试")
        sys.exit(1)


if __name__ == "__main__":
    main()

database.py

python 复制代码
import sqlite3
import os
from datetime import datetime
from typing import Optional, List, Dict, Any


class BlogDatabase:
    """博客数据库管理类"""
    
    def __init__(self, db_path: str = "blog.db"):
        """初始化数据库连接
        
        Args:
            db_path: 数据库文件路径
        """
        self.db_path = db_path
        self.init_database()
    
    def init_database(self):
        """初始化数据库表结构"""
        with sqlite3.connect(self.db_path) as conn:
            cursor = conn.cursor()
            
            # 创建博客文章表
            cursor.execute('''
                CREATE TABLE IF NOT EXISTS posts (
                    id INTEGER PRIMARY KEY AUTOINCREMENT,
                    title TEXT NOT NULL,
                    content TEXT NOT NULL,
                    author TEXT DEFAULT '管理员',
                    category TEXT DEFAULT '未分类',
                    tags TEXT DEFAULT '',
                    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
                    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
                    is_published BOOLEAN DEFAULT 1,
                    view_count INTEGER DEFAULT 0
                )
            ''')
            
            # 创建附件表
            cursor.execute('''
                CREATE TABLE IF NOT EXISTS attachments (
                    id INTEGER PRIMARY KEY AUTOINCREMENT,
                    filename TEXT NOT NULL,
                    original_filename TEXT NOT NULL,
                    file_path TEXT NOT NULL,
                    file_size INTEGER NOT NULL,
                    file_type TEXT NOT NULL,
                    post_id INTEGER,
                    uploaded_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
                    download_count INTEGER DEFAULT 0,
                    FOREIGN KEY (post_id) REFERENCES posts (id) ON DELETE CASCADE
                )
            ''')
            
            # 创建索引以提高查询性能
            cursor.execute('''
                CREATE INDEX IF NOT EXISTS idx_posts_title ON posts(title)
            ''')
            cursor.execute('''
                CREATE INDEX IF NOT EXISTS idx_posts_category ON posts(category)
            ''')
            cursor.execute('''
                CREATE INDEX IF NOT EXISTS idx_posts_created_at ON posts(created_at)
            ''')
            cursor.execute('''
                CREATE INDEX IF NOT EXISTS idx_attachments_post_id ON attachments(post_id)
            ''')
            cursor.execute('''
                CREATE INDEX IF NOT EXISTS idx_attachments_filename ON attachments(filename)
            ''')
            
            conn.commit()
            print("数据库初始化完成!")
    
    def get_connection(self):
        """获取数据库连接"""
        return sqlite3.connect(self.db_path)


if __name__ == "__main__":
    # 测试数据库初始化
    db = BlogDatabase()
    print("博客数据库创建成功!")

blog_manager.py

python 复制代码
import sqlite3
from datetime import datetime
from typing import Optional, List, Dict, Any, Tuple
from database import BlogDatabase
from attachment_manager import AttachmentManager


class BlogManager:
    """博客文章管理类,提供完整的增删改查功能"""
    
    def __init__(self, db_path: str = "blog.db"):
        """初始化博客管理器
        
        Args:
            db_path: 数据库文件路径
        """
        self.db = BlogDatabase(db_path)
        self.attachment_manager = AttachmentManager(db_path)
    
    def create_post(self, title: str, content: str, author: str = "管理员", 
                   category: str = "技术", tags: str = "", 
                   is_published: bool = True) -> bool:
        """创建新的博客文章
        
        Args:
            title: 文章标题
            content: 文章内容
            author: 作者
            category: 分类
            tags: 标签(多个标签用逗号分隔)
            is_published: 是否发布
            
        Returns:
            bool: 创建是否成功
        """
        try:
            with self.db.get_connection() as conn:
                cursor = conn.cursor()
                cursor.execute('''
                    INSERT INTO posts (title, content, author, category, tags, is_published)
                    VALUES (?, ?, ?, ?, ?, ?)
                ''', (title, content, author, category, tags, is_published))
                conn.commit()
                return True
        except sqlite3.Error as e:
            print(f"创建文章失败: {e}")
            return False
    
    def get_post_by_id(self, post_id: int) -> Optional[Dict[str, Any]]:
        """根据ID获取文章详情
        
        Args:
            post_id: 文章ID
            
        Returns:
            Dict: 文章信息字典,如果不存在返回None
        """
        try:
            with self.db.get_connection() as conn:
                cursor = conn.cursor()
                cursor.execute('''
                    SELECT * FROM posts WHERE id = ?
                ''', (post_id,))
                row = cursor.fetchone()
                
                if row:
                    # 增加浏览次数
                    cursor.execute('''
                        UPDATE posts SET view_count = view_count + 1 WHERE id = ?
                    ''', (post_id,))
                    conn.commit()
                    
                    post_data = {
                        'id': row[0],
                        'title': row[1],
                        'content': row[2],
                        'author': row[3],
                        'category': row[4],
                        'tags': row[5],
                        'created_at': row[6],
                        'updated_at': row[7],
                        'is_published': bool(row[8]),
                        'view_count': row[9] + 1
                    }
                    
                    # 获取文章的附件
                    post_data['attachments'] = self.attachment_manager.get_attachments_by_post(post_id)
                    
                    return post_data
                return None
        except sqlite3.Error as e:
            print(f"获取文章失败: {e}")
            return None
    
    def get_all_posts(self, limit: Optional[int] = None, 
                     offset: int = 0, published_only: bool = True) -> List[Dict[str, Any]]:
        """获取所有文章列表
        
        Args:
            limit: 限制返回数量
            offset: 偏移量
            published_only: 是否只返回已发布的文章
            
        Returns:
            List[Dict]: 文章列表
        """
        try:
            with self.db.get_connection() as conn:
                cursor = conn.cursor()
                
                query = "SELECT * FROM posts"
                params = []
                
                if published_only:
                    query += " WHERE is_published = 1"
                
                query += " ORDER BY created_at DESC"
                
                if limit:
                    query += " LIMIT ? OFFSET ?"
                    params.extend([limit, offset])
                
                cursor.execute(query, params)
                rows = cursor.fetchall()
                
                posts = []
                for row in rows:
                    posts.append({
                        'id': row[0],
                        'title': row[1],
                        'content': row[2][:200] + "..." if len(row[2]) > 200 else row[2],  # 列表显示截取内容
                        'full_content': row[2],  # 完整内容
                        'author': row[3],
                        'category': row[4],
                        'tags': row[5],
                        'created_at': row[6],
                        'updated_at': row[7],
                        'is_published': bool(row[8]),
                        'view_count': row[9]
                    })
                
                return posts
        except sqlite3.Error as e:
            print(f"获取文章列表失败: {e}")
            return []
    
    def update_post(self, post_id: int, title: Optional[str] = None, 
                   content: Optional[str] = None, author: Optional[str] = None,
                   category: Optional[str] = None, tags: Optional[str] = None,
                   is_published: Optional[bool] = None) -> bool:
        """更新文章信息
        
        Args:
            post_id: 文章ID
            title: 新标题
            content: 新内容
            author: 新作者
            category: 新分类
            tags: 新标签
            is_published: 发布状态
            
        Returns:
            bool: 更新是否成功
        """
        try:
            with self.db.get_connection() as conn:
                cursor = conn.cursor()
                
                # 构建动态更新查询
                update_fields = []
                params = []
                
                if title is not None:
                    update_fields.append("title = ?")
                    params.append(title)
                
                if content is not None:
                    update_fields.append("content = ?")
                    params.append(content)
                
                if author is not None:
                    update_fields.append("author = ?")
                    params.append(author)
                
                if category is not None:
                    update_fields.append("category = ?")
                    params.append(category)
                
                if tags is not None:
                    update_fields.append("tags = ?")
                    params.append(tags)
                
                if is_published is not None:
                    update_fields.append("is_published = ?")
                    params.append(is_published)
                
                if update_fields:
                    update_fields.append("updated_at = CURRENT_TIMESTAMP")
                    query = f"UPDATE posts SET {', '.join(update_fields)} WHERE id = ?"
                    params.append(post_id)
                    
                    cursor.execute(query, params)
                    conn.commit()
                    return cursor.rowcount > 0
                
                return False
        except sqlite3.Error as e:
            print(f"更新文章失败: {e}")
            return False
    
    def delete_post(self, post_id: int) -> bool:
        """删除文章
        
        Args:
            post_id: 文章ID
            
        Returns:
            bool: 删除是否成功
        """
        try:
            with self.db.get_connection() as conn:
                cursor = conn.cursor()
                cursor.execute("DELETE FROM posts WHERE id = ?", (post_id,))
                conn.commit()
                return cursor.rowcount > 0
        except sqlite3.Error as e:
            print(f"删除文章失败: {e}")
            return False
    
    def search_posts(self, keyword: str, search_in: str = "all") -> List[Dict[str, Any]]:
        """搜索文章
        
        Args:
            keyword: 搜索关键词
            search_in: 搜索范围 ("title", "content", "all")
            
        Returns:
            List[Dict]: 搜索结果列表
        """
        try:
            with self.db.get_connection() as conn:
                cursor = conn.cursor()
                
                if search_in == "title":
                    query = "SELECT * FROM posts WHERE title LIKE ? AND is_published = 1 ORDER BY created_at DESC"
                elif search_in == "content":
                    query = "SELECT * FROM posts WHERE content LIKE ? AND is_published = 1 ORDER BY created_at DESC"
                else:  # all
                    query = "SELECT * FROM posts WHERE (title LIKE ? OR content LIKE ?) AND is_published = 1 ORDER BY created_at DESC"
                
                keyword_pattern = f"%{keyword}%"
                
                if search_in == "all":
                    cursor.execute(query, (keyword_pattern, keyword_pattern))
                else:
                    cursor.execute(query, (keyword_pattern,))
                
                rows = cursor.fetchall()
                
                posts = []
                for row in rows:
                    posts.append({
                        'id': row[0],
                        'title': row[1],
                        'content': row[2][:200] + "..." if len(row[2]) > 200 else row[2],
                        'full_content': row[2],
                        'author': row[3],
                        'category': row[4],
                        'tags': row[5],
                        'created_at': row[6],
                        'updated_at': row[7],
                        'is_published': bool(row[8]),
                        'view_count': row[9]
                    })
                
                return posts
        except sqlite3.Error as e:
            print(f"搜索文章失败: {e}")
            return []
    
    def get_categories(self) -> List[str]:
        """获取所有分类"""
        try:
            with self.db.get_connection() as conn:
                cursor = conn.cursor()
                cursor.execute("SELECT DISTINCT category FROM posts WHERE is_published = 1")
                categories = [row[0] for row in cursor.fetchall()]
                return categories
        except sqlite3.Error as e:
            print(f"获取分类失败: {e}")
            return []
    
    def increment_view_count(self, post_id: int) -> bool:
        """增加文章浏览量
        
        Args:
            post_id: 文章ID
            
        Returns:
            bool: 操作是否成功
        """
        try:
            with self.db.get_connection() as conn:
                cursor = conn.cursor()
                cursor.execute('''
                    UPDATE posts SET view_count = view_count + 1 WHERE id = ?
                ''', (post_id,))
                conn.commit()
                return cursor.rowcount > 0
        except sqlite3.Error as e:
            print(f"增加浏览量失败: {e}")
            return False
    
    def get_posts_by_category(self, category: str) -> List[Dict[str, Any]]:
        """按分类获取文章"""
        try:
            with self.db.get_connection() as conn:
                cursor = conn.cursor()
                cursor.execute('''
                    SELECT * FROM posts WHERE category = ? AND is_published = 1 
                    ORDER BY created_at DESC
                ''', (category,))
                rows = cursor.fetchall()
                
                posts = []
                for row in rows:
                    posts.append({
                        'id': row[0],
                        'title': row[1],
                        'content': row[2][:200] + "..." if len(row[2]) > 200 else row[2],
                        'full_content': row[2],
                        'author': row[3],
                        'category': row[4],
                        'tags': row[5],
                        'created_at': row[6],
                        'updated_at': row[7],
                        'is_published': bool(row[8]),
                        'view_count': row[9]
                    })
                
                return posts
        except sqlite3.Error as e:
            print(f"按分类获取文章失败: {e}")
            return []
    
    # =============附件相关方法=============
    
    def upload_attachment(self, file_path: str, original_filename: str, 
                         post_id: Optional[int] = None) -> Optional[Dict[str, Any]]:
        """上传附件
        
        Args:
            file_path: 临时文件路径
            original_filename: 原始文件名
            post_id: 关联的文章ID
            
        Returns:
            Dict: 附件信息
        """
        return self.attachment_manager.upload_file(file_path, original_filename, post_id)
    
    def get_post_attachments(self, post_id: int) -> List[Dict[str, Any]]:
        """获取文章的所有附件"""
        return self.attachment_manager.get_attachments_by_post(post_id)
    
    def get_all_attachments(self) -> List[Dict[str, Any]]:
        """获取所有附件"""
        return self.attachment_manager.get_all_attachments()
    
    def delete_attachment(self, attachment_id: int) -> bool:
        """删除附件"""
        return self.attachment_manager.delete_attachment(attachment_id)
    
    def download_attachment(self, attachment_id: int) -> Optional[str]:
        """下载附件(返回文件路径)"""
        return self.attachment_manager.download_file(attachment_id)
    
    def get_attachment_info(self, attachment_id: int) -> Optional[Dict[str, Any]]:
        """获取附件信息"""
        return self.attachment_manager.get_attachment_by_id(attachment_id)
    
    def format_file_size(self, size_bytes: int) -> str:
        """格式化文件大小"""
        return self.attachment_manager.get_file_size_formatted(size_bytes)


if __name__ == "__main__":
    # 测试博客管理器
    blog_manager = BlogManager()
    
    # 测试创建文章
    success = blog_manager.create_post(
        title="测试文章",
        content="这是一篇测试文章的内容。",
        author="测试用户",
        category="技术",
        tags="Python,博客,测试"
    )
    
    if success:
        print("测试文章创建成功!")
        
        # 测试获取所有文章
        posts = blog_manager.get_all_posts()
        print(f"当前共有 {len(posts)} 篇文章")
        
        for post in posts:
            print(f"- {post['title']} (ID: {post['id']})")
    else:
        print("测试文章创建失败!")

attachment_manager.py

python 复制代码
import os
import shutil
import sqlite3
import hashlib
import mimetypes
from datetime import datetime
from pathlib import Path
from typing import Optional, List, Dict, Any, Tuple
from database import BlogDatabase


class AttachmentManager:
    """附件管理类,处理文件上传、下载、删除等操作"""
    
    def __init__(self, db_path: str = "blog.db", upload_dir: str = "uploads"):
        """初始化附件管理器
        
        Args:
            db_path: 数据库文件路径
            upload_dir: 文件上传目录
        """
        self.db = BlogDatabase(db_path)
        self.upload_dir = Path(upload_dir)
        self.max_file_size = 50 * 1024 * 1024  # 50MB
        self.allowed_extensions = {
            'txt', 'pdf', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx',
            'jpg', 'jpeg', 'png', 'gif', 'bmp', 'svg',
            'mp3', 'wav', 'mp4', 'avi', 'mov',
            'zip', 'rar', '7z', 'tar', 'gz',
            'py', 'js', 'html', 'css', 'json', 'xml', 'md'
        }
        self._ensure_upload_dir()
    
    def _ensure_upload_dir(self):
        """确保上传目录存在"""
        self.upload_dir.mkdir(exist_ok=True)
        
        # 创建按年月分组的子目录
        current_date = datetime.now()
        year_month_dir = self.upload_dir / f"{current_date.year}" / f"{current_date.month:02d}"
        year_month_dir.mkdir(parents=True, exist_ok=True)
    
    def _get_file_hash(self, file_path: str) -> str:
        """计算文件哈希值"""
        hash_md5 = hashlib.md5()
        with open(file_path, "rb") as f:
            for chunk in iter(lambda: f.read(4096), b""):
                hash_md5.update(chunk)
        return hash_md5.hexdigest()
    
    def _get_safe_filename(self, original_filename: str) -> str:
        """生成安全的文件名"""
        # 获取文件扩展名
        file_ext = Path(original_filename).suffix.lower()
        
        # 生成时间戳和随机字符串
        timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
        
        # 使用原始文件名的哈希值作为唯一标识
        filename_hash = hashlib.md5(original_filename.encode()).hexdigest()[:8]
        
        return f"{timestamp}_{filename_hash}{file_ext}"
    
    def _is_allowed_file(self, filename: str) -> bool:
        """检查文件扩展名是否允许"""
        ext = Path(filename).suffix.lower().lstrip('.')
        return ext in self.allowed_extensions
    
    def upload_file(self, file_path: str, original_filename: str, 
                   post_id: Optional[int] = None) -> Optional[Dict[str, Any]]:
        """上传文件
        
        Args:
            file_path: 临时文件路径
            original_filename: 原始文件名
            post_id: 关联的文章ID(可选)
            
        Returns:
            Dict: 附件信息,失败返回None
        """
        try:
            # 验证文件
            if not os.path.exists(file_path):
                raise ValueError("文件不存在")
            
            if not self._is_allowed_file(original_filename):
                raise ValueError(f"不支持的文件类型: {Path(original_filename).suffix}")
            
            # 检查文件大小
            file_size = os.path.getsize(file_path)
            if file_size > self.max_file_size:
                raise ValueError(f"文件过大,最大支持 {self.max_file_size // (1024*1024)}MB")
            
            # 生成安全的文件名
            safe_filename = self._get_safe_filename(original_filename)
            
            # 创建目标路径
            current_date = datetime.now()
            target_dir = self.upload_dir / f"{current_date.year}" / f"{current_date.month:02d}"
            target_dir.mkdir(parents=True, exist_ok=True)
            target_path = target_dir / safe_filename
            
            # 复制文件
            shutil.copy2(file_path, target_path)
            
            # 获取文件信息
            file_type = mimetypes.guess_type(original_filename)[0] or 'application/octet-stream'
            relative_path = str(target_path.relative_to(self.upload_dir))
            
            # 保存到数据库
            with self.db.get_connection() as conn:
                cursor = conn.cursor()
                cursor.execute('''
                    INSERT INTO attachments 
                    (filename, original_filename, file_path, file_size, file_type, post_id)
                    VALUES (?, ?, ?, ?, ?, ?)
                ''', (safe_filename, original_filename, relative_path, file_size, file_type, post_id))
                
                attachment_id = cursor.lastrowid
                conn.commit()
                
                return {
                    'id': attachment_id,
                    'filename': safe_filename,
                    'original_filename': original_filename,
                    'file_path': relative_path,
                    'file_size': file_size,
                    'file_type': file_type,
                    'post_id': post_id,
                    'uploaded_at': datetime.now().isoformat(),
                    'download_count': 0
                }
                
        except Exception as e:
            print(f"文件上传失败: {e}")
            return None
    
    def get_attachment_by_id(self, attachment_id: int) -> Optional[Dict[str, Any]]:
        """根据ID获取附件信息"""
        try:
            with self.db.get_connection() as conn:
                cursor = conn.cursor()
                cursor.execute('''
                    SELECT * FROM attachments WHERE id = ?
                ''', (attachment_id,))
                row = cursor.fetchone()
                
                if row:
                    return {
                        'id': row[0],
                        'filename': row[1],
                        'original_filename': row[2],
                        'file_path': row[3],
                        'file_size': row[4],
                        'file_type': row[5],
                        'post_id': row[6],
                        'uploaded_at': row[7],
                        'download_count': row[8]
                    }
                return None
        except sqlite3.Error as e:
            print(f"获取附件信息失败: {e}")
            return None
    
    def get_attachments_by_post(self, post_id: int) -> List[Dict[str, Any]]:
        """获取文章的所有附件"""
        try:
            with self.db.get_connection() as conn:
                cursor = conn.cursor()
                cursor.execute('''
                    SELECT * FROM attachments WHERE post_id = ? ORDER BY uploaded_at DESC
                ''', (post_id,))
                rows = cursor.fetchall()
                
                attachments = []
                for row in rows:
                    attachments.append({
                        'id': row[0],
                        'filename': row[1],
                        'original_filename': row[2],
                        'file_path': row[3],
                        'file_size': row[4],
                        'file_type': row[5],
                        'post_id': row[6],
                        'uploaded_at': row[7],
                        'download_count': row[8]
                    })
                
                return attachments
        except sqlite3.Error as e:
            print(f"获取文章附件失败: {e}")
            return []
    
    def get_all_attachments(self) -> List[Dict[str, Any]]:
        """获取所有附件"""
        try:
            with self.db.get_connection() as conn:
                cursor = conn.cursor()
                cursor.execute('''
                    SELECT a.*, p.title as post_title 
                    FROM attachments a 
                    LEFT JOIN posts p ON a.post_id = p.id 
                    ORDER BY a.uploaded_at DESC
                ''')
                rows = cursor.fetchall()
                
                attachments = []
                for row in rows:
                    attachments.append({
                        'id': row[0],
                        'filename': row[1],
                        'original_filename': row[2],
                        'file_path': row[3],
                        'file_size': row[4],
                        'file_type': row[5],
                        'post_id': row[6],
                        'uploaded_at': row[7],
                        'download_count': row[8],
                        'post_title': row[9] or '独立附件'
                    })
                
                return attachments
        except sqlite3.Error as e:
            print(f"获取所有附件失败: {e}")
            return []
    
    def delete_attachment(self, attachment_id: int) -> bool:
        """删除附件"""
        try:
            # 先获取附件信息
            attachment = self.get_attachment_by_id(attachment_id)
            if not attachment:
                return False
            
            # 删除物理文件
            file_path = self.upload_dir / attachment['file_path']
            if file_path.exists():
                file_path.unlink()
            
            # 从数据库删除记录
            with self.db.get_connection() as conn:
                cursor = conn.cursor()
                cursor.execute("DELETE FROM attachments WHERE id = ?", (attachment_id,))
                conn.commit()
                return cursor.rowcount > 0
                
        except Exception as e:
            print(f"删除附件失败: {e}")
            return False
    
    def download_file(self, attachment_id: int) -> Optional[str]:
        """下载文件(返回文件路径)"""
        try:
            attachment = self.get_attachment_by_id(attachment_id)
            if not attachment:
                return None
            
            file_path = self.upload_dir / attachment['file_path']
            if not file_path.exists():
                return None
            
            # 增加下载计数
            with self.db.get_connection() as conn:
                cursor = conn.cursor()
                cursor.execute('''
                    UPDATE attachments SET download_count = download_count + 1 
                    WHERE id = ?
                ''', (attachment_id,))
                conn.commit()
            
            return str(file_path)
            
        except Exception as e:
            print(f"下载文件失败: {e}")
            return None
    
    def get_file_size_formatted(self, size_bytes: int) -> str:
        """格式化文件大小"""
        if size_bytes < 1024:
            return f"{size_bytes} B"
        elif size_bytes < 1024 * 1024:
            return f"{size_bytes / 1024:.1f} KB"
        elif size_bytes < 1024 * 1024 * 1024:
            return f"{size_bytes / (1024 * 1024):.1f} MB"
        else:
            return f"{size_bytes / (1024 * 1024 * 1024):.1f} GB"
    
    def cleanup_orphaned_files(self):
        """清理孤立的文件(数据库中不存在的文件)"""
        try:
            # 获取数据库中所有文件路径
            with self.db.get_connection() as conn:
                cursor = conn.cursor()
                cursor.execute("SELECT file_path FROM attachments")
                db_files = {row[0] for row in cursor.fetchall()}
            
            # 扫描上传目录中的所有文件
            cleanup_count = 0
            for file_path in self.upload_dir.rglob("*"):
                if file_path.is_file():
                    relative_path = str(file_path.relative_to(self.upload_dir))
                    if relative_path not in db_files:
                        file_path.unlink()
                        cleanup_count += 1
            
            print(f"清理了 {cleanup_count} 个孤立文件")
            return cleanup_count
            
        except Exception as e:
            print(f"清理孤立文件失败: {e}")
            return 0


if __name__ == "__main__":
    # 测试附件管理器
    attachment_manager = AttachmentManager()
    print("附件管理器初始化成功!")
    
    # 获取所有附件
    attachments = attachment_manager.get_all_attachments()
    print(f"当前共有 {len(attachments)} 个附件")
    
    for attachment in attachments:
        size_formatted = attachment_manager.get_file_size_formatted(attachment['file_size'])
        print(f"- {attachment['original_filename']} ({size_formatted}) - {attachment['post_title']}")

gradio_interface.py

python 复制代码
import gradio as gr
import pandas as pd
import qrcode
import io
import base64
from datetime import datetime
from typing import List, Dict, Any, Tuple, Optional
from blog_manager import BlogManager
import os
from pathlib import Path


class BlogGradioInterface:
    """博客管理系统的Gradio界面类"""
    
    def __init__(self):
        """初始化界面组件"""
        self.blog_manager = BlogManager()
        self.current_post_id = None
    
    def create_post_interface(self, title: str, content: str, author: str, 
                            category: str, tags: str) -> Tuple[str, str]:
        """创建文章的界面函数
        
        Args:
            title: 文章标题
            content: 文章内容
            author: 作者
            category: 分类
            tags: 标签
            
        Returns:
            Tuple[str, str]: (状态消息, 更新后的文章列表)
        """
        if not title.strip():
            return "❌ 标题不能为空!", self.get_posts_display()
        
        if not content.strip():
            return "❌ 内容不能为空!", self.get_posts_display()
        
        success = self.blog_manager.create_post(
            title=title.strip(),
            content=content.strip(),
            author=author.strip() or "管理员",
            category=category.strip() or "未分类",
            tags=tags.strip()
        )
        
        if success:
            return "✅ 文章创建成功!", self.get_posts_display()
        else:
            return "❌ 文章创建失败!", self.get_posts_display()
    
    def get_posts_display(self) -> str:
        """获取文章列表的显示内容"""
        posts = self.blog_manager.get_all_posts()
        
        if not posts:
            return "暂无文章"
        
        display_content = "## 📚 博客文章列表\n\n"
        
        for post in posts:
            # 获取文章的附件
            attachments = self.blog_manager.get_post_attachments(post['id'])
            attachment_info = ""
            if attachments:
                attachment_count = len(attachments)
                attachment_info = f" | 📁 附件: {attachment_count}个"
            
            # 内容预览(限制长度)
            content_preview = post['content']
            if len(content_preview) > 200:
                content_preview = content_preview[:200] + "..."
            
            display_content += f"""
### 📝 {post['title']}
**作者:** {post['author']} | **分类:** {post['category']} | **浏览量:** {post['view_count']}{attachment_info}  
**创建时间:** {post['created_at']} | **ID:** {post['id']}  
**标签:** {post['tags'] or '无'}  
**内容预览:** {content_preview}

---
"""
        

        
        return display_content
    
    def view_full_post_interface(self, post_id: str) -> Tuple[str, str, str, str, str, str, str, str]:
        """查看文章完整内容的界面函数
        
        Args:
            post_id: 文章ID
            
        Returns:
            Tuple: (状态消息, 标题, 内容, 作者, 分类, 标签, 统计信息, Markdown预览)
        """
        try:
            pid = int(post_id.strip())
            post = self.blog_manager.get_post_by_id(pid)
            
            if post:
                # 获取文章的附件
                attachments = self.blog_manager.get_post_attachments(pid)
                attachment_info = ""
                if attachments:
                    attachment_count = len(attachments)
                    attachment_info = f"\n\n### 📁 附件列表 ({attachment_count}个)\n"
                    for attachment in attachments:
                        file_size = self.blog_manager.format_file_size(attachment['file_size'])
                        attachment_info += f"- 📄 {attachment['original_filename']} ({file_size}) - 下载次数: {attachment['download_count']}\n"
                
                # 统计信息
                stats_info = f"""
### 📊 文章统计
- **浏览量:** {post['view_count']} (刚刚增加)
- **创建时间:** {post['created_at']}
- **最后更新:** {post['updated_at']}
- **文章ID:** {post['id']}
"""
                
                # 生成Markdown预览内容
                markdown_preview = f"""
# {post['title']}

**作者:** {post['author']} | **分类:** {post['category']} | **浏览量:** {post['view_count']}  
**创建时间:** {post['created_at']} | **最后更新:** {post['updated_at']}  
**标签:** {post['tags'] or '无'}  

---

{post['content']}

{attachment_info if attachments else ''}
"""
                
                return (
                    f"✅ 正在查看文章: {post['title']}",
                    post['title'],
                    post['content'],
                    post['author'],
                    post['category'],
                    post['tags'],
                    stats_info + attachment_info,
                    markdown_preview
                )
            else:
                return "❌ 找不到指定的文章", "", "", "", "", "", "", ""
        except ValueError:
            return "❌ 请输入有效的文章ID", "", "", "", "", "", "", ""
    
    def generate_qr_interface(self, ip_address: str, post_id: str) -> Tuple[str, str]:
        """生成文章二维码的界面函数
        
        Args:
            ip_address: 服务器IP地址
            post_id: 文章ID(可选,用于生成临时文件名)
            
        Returns:
            Tuple: (二维码图片路径, 文章链接)
        """
        try:
            # 验证IP地址
            if not ip_address or not ip_address.strip():
                return None, "❌ 请输入有效的服务器IP地址"
            
            ip_address = ip_address.strip()
            
            # 生成文章链接(使用用户输入的IP地址)
            article_url = f"http://{ip_address}"
            
            # 使用文章ID生成临时文件名,如果没有文章ID则使用默认名称
            if post_id and post_id.strip():
                temp_qr_path = f"temp_qr_{post_id.strip()}.png"
            else:
                temp_qr_path = "temp_qr_default.png"
            
            # 创建二维码
            qr = qrcode.QRCode(
                version=1,
                error_correction=qrcode.constants.ERROR_CORRECT_L,
                box_size=10,
                border=4,
            )
            qr.add_data(article_url)
            qr.make(fit=True)
            
            # 生成二维码图片
            qr_img = qr.make_image(fill_color="black", back_color="white")
            
            # 保存为临时文件
            qr_img.save(temp_qr_path)
            
            return temp_qr_path, article_url
        except Exception as e:
            print(f"生成二维码时出错: {e}")
            return None, f"❌ 生成二维码失败: {str(e)}"
    
    def search_posts_interface(self, keyword: str, search_in: str) -> str:
        """搜索文章的界面函数"""
        if not keyword.strip():
            return self.get_posts_display()
        
        posts = self.blog_manager.search_posts(keyword.strip(), search_in)
        
        if not posts:
            return f"🔍 没有找到包含 '{keyword}' 的文章"
        
        display_content = f"## 🔍 搜索结果 (关键词: '{keyword}')\n\n"
        
        for post in posts:
            # 内容预览(限制长度)
            content_preview = post['content']
            if len(content_preview) > 200:
                content_preview = content_preview[:200] + "..."
            
            display_content += f"""
### 📝 {post['title']}
**作者:** {post['author']} | **分类:** {post['category']} | **浏览量:** {post['view_count']}  
**创建时间:** {post['created_at']} | **ID:** {post['id']}  
**标签:** {post['tags'] or '无'}  
**内容预览:** {content_preview}

---
"""
        
        return display_content
    
    def get_post_details(self, post_id: str) -> Tuple[str, str, str, str, str, str]:
        """获取文章详情用于编辑
        
        Args:
            post_id: 文章ID
            
        Returns:
            Tuple: (状态消息, 标题, 内容, 作者, 分类, 标签)
        """
        try:
            pid = int(post_id.strip())
            post = self.blog_manager.get_post_by_id(pid)
            
            if post:
                self.current_post_id = pid
                return (
                    f"✅ 已加载文章: {post['title']}",
                    post['title'],
                    post['content'],
                    post['author'],
                    post['category'],
                    post['tags']
                )
            else:
                return "❌ 找不到指定的文章", "", "", "", "", ""
        except ValueError:
            return "❌ 请输入有效的文章ID", "", "", "", "", ""
    
    def update_post_interface(self, title: str, content: str, author: str, 
                            category: str, tags: str) -> Tuple[str, str]:
        """更新文章的界面函数"""
        if self.current_post_id is None:
            return "❌ 请先选择要编辑的文章", self.get_posts_display()
        
        if not title.strip():
            return "❌ 标题不能为空!", self.get_posts_display()
        
        if not content.strip():
            return "❌ 内容不能为空!", self.get_posts_display()
        
        success = self.blog_manager.update_post(
            post_id=self.current_post_id,
            title=title.strip(),
            content=content.strip(),
            author=author.strip(),
            category=category.strip(),
            tags=tags.strip()
        )
        
        if success:
            self.current_post_id = None
            return "✅ 文章更新成功!", self.get_posts_display()
        else:
            return "❌ 文章更新失败!", self.get_posts_display()
    
    def delete_post_interface(self, post_id: str) -> Tuple[str, str]:
        """删除文章的界面函数"""
        try:
            pid = int(post_id.strip())
            
            # 先获取文章信息用于确认
            post = self.blog_manager.get_post_by_id(pid)
            if not post:
                return "❌ 找不到指定的文章", self.get_posts_display()
            
            success = self.blog_manager.delete_post(pid)
            
            if success:
                return f"✅ 文章 '{post['title']}' 删除成功!", self.get_posts_display()
            else:
                return "❌ 文章删除失败!", self.get_posts_display()
        except ValueError:
            return "❌ 请输入有效的文章ID", self.get_posts_display()
    
    def get_categories_interface(self) -> List[str]:
        """获取分类列表"""
        categories = self.blog_manager.get_categories()
        return ["全部"] + categories
    
    def filter_by_category_interface(self, category: str) -> str:
        """按分类筛选文章"""
        if category == "全部":
            return self.get_posts_display()
        
        posts = self.blog_manager.get_posts_by_category(category)
        
        if not posts:
            return f"📂 分类 '{category}' 下暂无文章"
        
        display_content = f"## 📂 分类: {category}\n\n"
        
        for post in posts:
            # 内容预览(限制长度)
            content_preview = post['content']
            if len(content_preview) > 200:
                content_preview = content_preview[:200] + "..."
            
            display_content += f"""
### 📝 {post['title']}
**作者:** {post['author']} | **分类:** {post['category']} | **浏览量:** {post['view_count']}  
**创建时间:** {post['created_at']} | **ID:** {post['id']}  
**标签:** {post['tags'] or '无'}  
**内容预览:** {content_preview}

---
"""
        
        return display_content
    
    # =============附件相关界面方法=============
    
    def upload_file_interface(self, file_obj, post_id_str: str = "") -> Tuple[str, str]:
        """上传文件的界面函数
        
        Args:
            file_obj: Gradio文件对象
            post_id_str: 文章ID字符串
            
        Returns:
            Tuple[str, str]: (状态消息, 附件列表)
        """
        if file_obj is None:
            return "❌ 请选择要上传的文件!", self.get_attachments_display()
        
        try:
            # 解析文章ID
            post_id = None
            if post_id_str.strip():
                try:
                    post_id = int(post_id_str.strip())
                    # 验证文章是否存在
                    post = self.blog_manager.get_post_by_id(post_id)
                    if not post:
                        return f"❌ 文章 ID {post_id} 不存在!", self.get_attachments_display()
                except ValueError:
                    return "❌ 请输入有效的文章ID!", self.get_attachments_display()
            
            # 上传文件
            attachment = self.blog_manager.upload_attachment(
                file_path=file_obj.name,
                original_filename=os.path.basename(file_obj.name),
                post_id=post_id
            )
            
            if attachment:
                file_size = self.blog_manager.format_file_size(attachment['file_size'])
                post_info = f" (关联文章 ID: {post_id})" if post_id else " (独立附件)"
                return f"✅ 文件 '{attachment['original_filename']}' 上传成功!{file_size}{post_info}", self.get_attachments_display()
            else:
                return "❌ 文件上传失败!请检查文件格式和大小。", self.get_attachments_display()
                
        except Exception as e:
            return f"❌ 上传错误: {str(e)}", self.get_attachments_display()
    
    def get_attachments_display(self) -> str:
        """获取附件列表的显示内容"""
        attachments = self.blog_manager.get_all_attachments()
        
        if not attachments:
            return "暂无附件"
        
        display_content = "## 📁 附件列表\n\n"
        
        for attachment in attachments:
            file_size = self.blog_manager.format_file_size(attachment['file_size'])
            post_title = attachment.get('post_title', '独立附件')
            
            display_content += f"""
### 📄 {attachment['original_filename']}
**大小:** {file_size} | **类型:** {attachment['file_type']} | **下载次数:** {attachment['download_count']}  
**上传时间:** {attachment['uploaded_at']} | **ID:** {attachment['id']}  
**关联文章:** {post_title}  

---
"""
        
        return display_content
    
    def delete_attachment_interface(self, attachment_id: str) -> Tuple[str, str]:
        """删除附件的界面函数"""
        try:
            aid = int(attachment_id.strip())
            
            # 先获取附件信息用于确认
            attachment = self.blog_manager.get_attachment_info(aid)
            if not attachment:
                return "❌ 找不到指定的附件", self.get_attachments_display()
            
            success = self.blog_manager.delete_attachment(aid)
            
            if success:
                return f"✅ 附件 '{attachment['original_filename']}' 删除成功!", self.get_attachments_display()
            else:
                return "❌ 附件删除失败!", self.get_attachments_display()
        except ValueError:
            return "❌ 请输入有效的附件ID", self.get_attachments_display()
    
    def download_attachment_interface(self, attachment_id: str) -> Tuple[str, Optional[str]]:
        """下载附件的界面函数"""
        try:
            aid = int(attachment_id.strip())
            
            attachment = self.blog_manager.get_attachment_info(aid)
            if not attachment:
                return "❌ 找不到指定的附件", None
            
            file_path = self.blog_manager.download_attachment(aid)
            
            if file_path and os.path.exists(file_path):
                return f"✅ 文件 '{attachment['original_filename']}' 准备下载", file_path
            else:
                return "❌ 文件不存在或下载失败!", None
        except ValueError:
            return "❌ 请输入有效的附件ID", None
    
    def create_interface(self):
        """创建完整的Gradio界面"""
        
        # 自定义CSS样式
        custom_css = """
        .gradio-container {
            font-family: 'Microsoft YaHei', Arial, sans-serif;
        }
        .main-header {
            text-align: center;
            color: #2c3e50;
            margin-bottom: 30px;
        }
        .tab-content {
            padding: 20px;
        }
        """
        
        with gr.Blocks(css=custom_css, title="博客管理系统") as interface:
            
            # 主标题
            gr.HTML("""
                <div class=\"main-header\">
                    <h1>📝 博客管理系统</h1>
                    <p>基于 Gradio + SQLite 的简单博客管理平台</p>
                </div>
            """)
            

            
            with gr.Tabs():
                
                # 文章浏览标签页
                with gr.TabItem("📚 浏览文章", id="browse"):
                    with gr.Row():
                        with gr.Column(scale=1):
                            gr.Markdown("### 🔍 搜索和筛选")
                            
                            # 搜索功能
                            with gr.Group():
                                search_keyword = gr.Textbox(
                                    label="搜索关键词", 
                                    placeholder="输入关键词搜索文章...",
                                    lines=1
                                )
                                search_in = gr.Radio(
                                    choices=["all", "title", "content"],
                                    value="all",
                                    label="搜索范围",
                                    info="选择在哪里搜索关键词"
                                )
                                search_btn = gr.Button("🔍 搜索", variant="primary")
                            
                            # 分类筛选
                            with gr.Group():
                                category_filter = gr.Dropdown(
                                    label="按分类筛选",
                                    choices=self.get_categories_interface(),
                                    value="全部",
                                    interactive=True
                                )
                                refresh_categories_btn = gr.Button("🔄 刷新分类")
                            
                            # 手机查看二维码功能
                            with gr.Group():
                                gr.Markdown("### 📱 手机查看")
                                qr_ip_address = gr.Textbox(
                                    label="服务器IP地址", 
                                    placeholder="输入服务器IP地址,如: 192.168.xx.xx:7860",
                                    value="192.168.31.238:7860",
                                    lines=1
                                )
                                qr_post_id = gr.Textbox(
                                    label="文章ID(可选)", 
                                    placeholder="输入文章ID用于生成文件名(可选)",
                                    lines=1
                                )
                                generate_qr_btn = gr.Button("📱 生成二维码", variant="primary")
                                qr_code_image = gr.Image(
                                    label="文章二维码",
                                    value=None,
                                    interactive=False,
                                    height=200,
                                    width=200
                                )
                                qr_url = gr.Textbox(
                                    label="文章链接",
                                    value="",
                                    interactive=False,
                                    lines=1
                                )
                        
                        with gr.Column(scale=2):
                            posts_display = gr.Markdown(
                                value=self.get_posts_display(),
                                label="文章列表"
                            )
                    
                    # 绑定事件
                    search_btn.click(
                        fn=self.search_posts_interface,
                        inputs=[search_keyword, search_in],
                        outputs=[posts_display]
                    )
                    
                    category_filter.change(
                        fn=self.filter_by_category_interface,
                        inputs=[category_filter],
                        outputs=[posts_display]
                    )
                    
                    refresh_categories_btn.click(
                        fn=lambda: gr.Dropdown(choices=self.get_categories_interface()),
                        outputs=[category_filter]
                    )
                    
                    # 绑定二维码生成事件
                    generate_qr_btn.click(
                        fn=self.generate_qr_interface,
                        inputs=[qr_ip_address, qr_post_id],
                        outputs=[qr_code_image, qr_url]
                    )
                
                # 查看文章完整内容标签页
                with gr.TabItem("📖 查看文章", id="view"):
                    gr.Markdown("### 📖 查看文章完整内容")
                    
                    with gr.Row():
                        with gr.Column(scale=1):
                            view_post_id = gr.Textbox(
                                label="文章ID", 
                                placeholder="输入要查看的文章ID",
                                lines=1
                            )
                            view_btn = gr.Button("📖 查看文章", variant="primary")
                            view_status = gr.Markdown()
                        
                        with gr.Column(scale=2):
                            gr.Markdown("### 文章信息")
                            view_title = gr.Textbox(label="标题", interactive=False)
                            view_author = gr.Textbox(label="作者", interactive=False)
                            view_category = gr.Textbox(label="分类", interactive=False)
                            view_tags = gr.Textbox(label="标签", interactive=False)
                    
                    # 文章内容显示区域
                    with gr.Row():
                        with gr.Column(scale=2):
                            # 添加Markdown预览功能
                            with gr.Tabs():
                                with gr.TabItem("📝 原始内容"):
                                    view_content_raw = gr.Textbox(
                                        label="文章内容(原始文本)",
                                        lines=15,
                                        interactive=False
                                    )
                                
                                with gr.TabItem("👀 Markdown预览"):
                                    view_content_preview = gr.Markdown(
                                        label="Markdown预览",
                                        value="## 文章预览\n\n请先加载文章内容..."
                                    )
                    
                    view_stats = gr.Markdown(label="统计信息")
                    
                    # 绑定查看事件
                    view_btn.click(
                        fn=self.view_full_post_interface,
                        inputs=[view_post_id],
                        outputs=[view_status, view_title, view_content_raw, view_author, view_category, view_tags, view_stats, view_content_preview]
                    )
                
                # 创建文章标签页
                with gr.TabItem("✍️ 创建文章", id="create"):
                    gr.Markdown("### ✍️ 写新文章")
                    
                    with gr.Row():
                        with gr.Column():
                            create_title = gr.Textbox(
                                label="文章标题", 
                                placeholder="输入文章标题...",
                                lines=1
                            )
                            create_author = gr.Textbox(
                                label="作者", 
                                value="管理员",
                                lines=1
                            )
                        
                        with gr.Column():
                            create_category = gr.Textbox(
                                label="分类", 
                                value="未分类",
                                lines=1
                            )
                            create_tags = gr.Textbox(
                                label="标签", 
                                placeholder="多个标签用逗号分隔",
                                lines=1
                            )
                    
                    create_content = gr.Textbox(
                        label="文章内容",
                        placeholder="在这里写下您的文章内容...",
                        lines=15
                    )
                    
                    with gr.Row():
                        create_btn = gr.Button("📝 发布文章", variant="primary", size="lg")
                        clear_btn = gr.Button("🗑️ 清空", variant="secondary")
                    
                    create_status = gr.Markdown()
                    posts_display_create = gr.Markdown()
                    
                    # 绑定创建文章事件
                    create_btn.click(
                        fn=self.create_post_interface,
                        inputs=[create_title, create_content, create_author, create_category, create_tags],
                        outputs=[create_status, posts_display_create]
                    )
                    
                    # 清空表单
                    clear_btn.click(
                        fn=lambda: ("", "", "管理员", "未分类", "", ""),
                        outputs=[create_title, create_content, create_author, create_category, create_tags, create_status]
                    )
                
                # 编辑文章标签页
                with gr.TabItem("✏️ 编辑文章", id="edit"):
                    gr.Markdown("### ✏️ 编辑现有文章")
                    
                    with gr.Row():
                        edit_post_id = gr.Textbox(
                            label="文章ID", 
                            placeholder="输入要编辑的文章ID",
                            lines=1
                        )
                        load_btn = gr.Button("📥 加载文章", variant="primary")
                    
                    edit_status = gr.Markdown()
                    
                    with gr.Row():
                        with gr.Column():
                            edit_title = gr.Textbox(label="文章标题", lines=1)
                            edit_author = gr.Textbox(label="作者", lines=1)
                        
                        with gr.Column():
                            edit_category = gr.Textbox(label="分类", lines=1)
                            edit_tags = gr.Textbox(label="标签", lines=1)
                    
                    edit_content = gr.Textbox(label="文章内容", lines=15)
                    
                    update_btn = gr.Button("💾 更新文章", variant="primary", size="lg")
                    posts_display_edit = gr.Markdown()
                    
                    # 绑定编辑相关事件
                    load_btn.click(
                        fn=self.get_post_details,
                        inputs=[edit_post_id],
                        outputs=[edit_status, edit_title, edit_content, edit_author, edit_category, edit_tags]
                    )
                    
                    update_btn.click(
                        fn=self.update_post_interface,
                        inputs=[edit_title, edit_content, edit_author, edit_category, edit_tags],
                        outputs=[edit_status, posts_display_edit]
                    )
                
                # 删除文章标签页
                with gr.TabItem("🗑️ 删除文章", id="delete"):
                    gr.Markdown("### 🗑️ 删除文章")
                    gr.Markdown("⚠️ **警告:删除操作不可恢复,请谨慎操作!**")
                    
                    delete_post_id = gr.Textbox(
                        label="文章ID", 
                        placeholder="输入要删除的文章ID",
                        lines=1
                    )
                    
                    delete_btn = gr.Button("🗑️ 删除文章", variant="stop", size="lg")
                    delete_status = gr.Markdown()
                    posts_display_delete = gr.Markdown()
                    
                    # 绑定删除事件
                    delete_btn.click(
                        fn=self.delete_post_interface,
                        inputs=[delete_post_id],
                        outputs=[delete_status, posts_display_delete]
                    )
                
                # 附件管理标签页
                with gr.TabItem("📁 附件管理", id="attachments"):
                    gr.Markdown("### 📁 文件附件管理")
                    
                    with gr.Tabs():
                        # 上传附件子标签页
                        with gr.TabItem("📤 上传附件"):
                            gr.Markdown("#### 📤 上传新附件")
                            
                            with gr.Row():
                                with gr.Column():
                                    upload_file = gr.File(
                                        label="选择文件",
                                        file_types=None,
                                        file_count="single"
                                    )
                                    upload_post_id = gr.Textbox(
                                        label="关联文章ID (可选)",
                                        placeholder="留空表示独立附件",
                                        lines=1
                                    )
                                
                                with gr.Column():
                                    gr.Markdown("""
                                    **支持的文件类型:**
                                    - 📄 文档: txt, pdf, doc, docx, xls, xlsx, ppt, pptx
                                    - 🖼️ 图片: jpg, jpeg, png, gif, bmp, svg
                                    - 🎵 音频: mp3, wav
                                    - 🎬 视频: mp4, avi, mov
                                    - 📦 压缩包: zip, rar, 7z, tar, gz
                                    - 💻 代码: py, js, html, css, json, xml, md
                                    
                                    **文件大小限制:** 50MB
                                    """)
                            
                            upload_btn = gr.Button("📤 上传文件", variant="primary", size="lg")
                            upload_status = gr.Markdown()
                            attachments_display_upload = gr.Markdown()
                            
                            # 绑定上传事件
                            upload_btn.click(
                                fn=self.upload_file_interface,
                                inputs=[upload_file, upload_post_id],
                                outputs=[upload_status, attachments_display_upload]
                            )
                        
                        # 浏览附件子标签页
                        with gr.TabItem("📋 浏览附件"):
                            gr.Markdown("#### 📋 所有附件列表")
                            
                            refresh_attachments_btn = gr.Button("🔄 刷新列表", variant="secondary")
                            attachments_display_browse = gr.Markdown(
                                value=self.get_attachments_display(),
                                label="附件列表"
                            )
                            
                            # 绑定刷新事件
                            refresh_attachments_btn.click(
                                fn=self.get_attachments_display,
                                outputs=[attachments_display_browse]
                            )
                        
                        # 下载附件子标签页
                        with gr.TabItem("📥 下载附件"):
                            gr.Markdown("#### 📥 下载附件文件")
                            
                            download_attachment_id = gr.Textbox(
                                label="附件ID",
                                placeholder="输入要下载的附件ID",
                                lines=1
                            )
                            
                            download_btn = gr.Button("📥 下载文件", variant="primary")
                            download_status = gr.Markdown()
                            download_file = gr.File(label="下载文件", visible=False)
                            
                            # 绑定下载事件
                            def handle_download(attachment_id):
                                status, file_path = self.download_attachment_interface(attachment_id)
                                if file_path:
                                    return status, gr.File(value=file_path, visible=True)
                                else:
                                    return status, gr.File(visible=False)
                            
                            download_btn.click(
                                fn=handle_download,
                                inputs=[download_attachment_id],
                                outputs=[download_status, download_file]
                            )
                        
                        # 删除附件子标签页
                        with gr.TabItem("🗑️ 删除附件"):
                            gr.Markdown("#### 🗑️ 删除附件文件")
                            gr.Markdown("⚠️ **警告:删除操作不可恢复,请谨慎操作!**")
                            
                            delete_attachment_id = gr.Textbox(
                                label="附件ID",
                                placeholder="输入要删除的附件ID",
                                lines=1
                            )
                            
                            delete_attachment_btn = gr.Button("🗑️ 删除附件", variant="stop", size="lg")
                            delete_attachment_status = gr.Markdown()
                            attachments_display_delete = gr.Markdown()
                            
                            # 绑定删除事件
                            delete_attachment_btn.click(
                                fn=self.delete_attachment_interface,
                                inputs=[delete_attachment_id],
                                outputs=[delete_attachment_status, attachments_display_delete]
                            )
        
        return interface


if __name__ == "__main__":
    # 创建并启动界面
    blog_interface = BlogGradioInterface()
    interface = blog_interface.create_interface()
    
    print("🚀 博客管理系统启动中...")
    print("📝 系统功能:")
    print("   - 📚 浏览和搜索文章")
    print("   - ✍️ 创建新文章") 
    print("   - ✏️ 编辑现有文章")
    print("   - 🗑️ 删除文章")
    print("   - 📂 按分类筛选")
    print("   - 📁 附件管理")
    
    interface.launch(
        server_name="0.0.0.0",
        server_port=7860,
        share=False,
        debug=True
    )

setup_storage.py

python 复制代码
import os
from pathlib import Path
import stat


def setup_upload_directory(upload_dir: str = "uploads"):
    """设置文件上传目录和安全性配置
    
    Args:
        upload_dir: 上传目录路径
    """
    upload_path = Path(upload_dir)
    
    try:
        # 创建主上传目录
        upload_path.mkdir(exist_ok=True)
        print(f"✅ 创建上传目录: {upload_path.absolute()}")
        
        # 创建年月子目录结构
        from datetime import datetime
        current_date = datetime.now()
        year_dir = upload_path / str(current_date.year)
        month_dir = year_dir / f"{current_date.month:02d}"
        month_dir.mkdir(parents=True, exist_ok=True)
        print(f"✅ 创建子目录: {month_dir.relative_to(upload_path)}")
        
        # 设置目录权限(仅限Unix/Linux系统)
        if os.name != 'nt':  # 非Windows系统
            # 设置目录权限为755(所有者读写执行,组和其他用户读执行)
            os.chmod(upload_path, stat.S_IRWXU | stat.S_IRGRP | stat.S_IXGRP | stat.S_IROTH | stat.S_IXOTH)
            print("✅ 设置目录权限")
        
        # 创建.gitignore文件防止上传文件被版本控制
        gitignore_path = upload_path / ".gitignore"
        if not gitignore_path.exists():
            with open(gitignore_path, "w", encoding="utf-8") as f:
                f.write("""# 忽略所有上传的文件
*
# 但不忽略目录
!*/
# 也不忽略这个.gitignore文件本身
!.gitignore
""")
            print("✅ 创建.gitignore文件")
        
        # 创建README文件说明目录用途
        readme_path = upload_path / "README.md"
        if not readme_path.exists():
            with open(readme_path, "w", encoding="utf-8") as f:
                f.write("""# 博客附件存储目录

此目录用于存储博客系统的上传附件。

## 目录结构
```
uploads/
├── 2024/
│   ├── 01/    # 2024年1月上传的文件
│   ├── 02/    # 2024年2月上传的文件
│   └── ...
├── 2025/
│   └── ...
├── .gitignore
└── README.md
```

## 安全提示
- 定期备份此目录中的重要文件
- 不要手动修改文件名,以免破坏数据库引用
- 如需清理旧文件,请使用系统提供的清理功能

## 文件命名规则
文件按以下格式命名:`YYYYMMDD_HHMMSS_hash.ext`
- YYYYMMDD_HHMMSS: 上传时间戳
- hash: 原文件名的8位哈希值
- ext: 原文件扩展名
""")
            print("✅ 创建README说明文件")
        
        # 检查可用磁盘空间
        stat_result = os.statvfs(upload_path) if hasattr(os, 'statvfs') else None
        if stat_result:
            # Unix/Linux系统
            available_bytes = stat_result.f_bavail * stat_result.f_frsize
            available_gb = available_bytes / (1024**3)
            print(f"📊 可用磁盘空间: {available_gb:.2f} GB")
        else:
            # Windows系统
            import shutil
            _, _, free_bytes = shutil.disk_usage(upload_path)
            free_gb = free_bytes / (1024**3)
            print(f"📊 可用磁盘空间: {free_gb:.2f} GB")
        
        print(f"🎉 文件存储目录设置完成!")
        return True
        
    except Exception as e:
        print(f"❌ 设置文件存储目录失败: {e}")
        return False


def validate_upload_security():
    """验证上传目录的安全性设置"""
    upload_path = Path("uploads")
    
    if not upload_path.exists():
        print("⚠️ 上传目录不存在")
        return False
    
    # 检查权限设置
    if os.name != 'nt':  # Unix/Linux系统
        dir_stat = upload_path.stat()
        # 检查是否具有写权限
        if not (dir_stat.st_mode & stat.S_IWUSR):
            print("⚠️ 目录缺少写权限")
            return False
    
    # 检查是否可以创建文件
    test_file = upload_path / "test_write.tmp"
    try:
        test_file.touch()
        test_file.unlink()  # 删除测试文件
        print("✅ 目录写权限验证成功")
    except Exception as e:
        print(f"❌ 目录写权限验证失败: {e}")
        return False
    
    return True


def cleanup_temp_files(upload_dir: str = "uploads"):
    """清理临时文件和测试文件"""
    upload_path = Path(upload_dir)
    
    if not upload_path.exists():
        return
    
    temp_patterns = ["*.tmp", "*~", ".DS_Store", "Thumbs.db"]
    cleaned_count = 0
    
    for pattern in temp_patterns:
        for temp_file in upload_path.rglob(pattern):
            try:
                temp_file.unlink()
                cleaned_count += 1
            except Exception:
                pass
    
    if cleaned_count > 0:
        print(f"🧹 清理了 {cleaned_count} 个临时文件")


if __name__ == "__main__":
    print("🔧 初始化文件存储系统...")
    print("=" * 50)
    
    # 设置上传目录
    setup_success = setup_upload_directory()
    
    if setup_success:
        # 验证安全性
        security_ok = validate_upload_security()
        
        if security_ok:
            # 清理临时文件
            cleanup_temp_files()
            print("=" * 50)
            print("✅ 文件存储系统初始化完成!")
        else:
            print("=" * 50)
            print("⚠️ 文件存储系统初始化完成,但存在安全问题")
    else:
        print("=" * 50)
        print("❌ 文件存储系统初始化失败!")

运行本机IP页 bat

bash 复制代码
@echo off
title 自动IP检测工具

echo 正在获取网络信息...
ipconfig
echo.

echo 正在搜索可用的IP地址...
setlocal enabledelayedexpansion
set ip_found=0

for /f "tokens=2 delims=:" %%i in ('ipconfig ^| findstr "IPv4"') do (
    set "ip=%%i"
    set "ip=!ip: =!"
    if not "!ip!"=="" (
        echo 找到IP地址: !ip!
        set found_ip=!ip!
        set ip_found=1
    )
)

if !ip_found! equ 0 (
    echo 自动搜索失败,使用手动输入...
    set /p found_ip="请输入服务器IP地址 (例如 192.168.1.100): "
)

if "!found_ip!"=="" (
    echo 错误:未指定IP地址!
    pause
    exit /b
)

set "url=http://!found_ip!:7860"
echo.
echo 即将打开: !url!
echo.
pause

start "" "!url!"
echo 操作完成!
pause
相关推荐
大模型铲屎官3 小时前
【LangChain 核心组件指南 | Agent篇】从零到精通:深度解析 create_agent 与 ReAct 智能体构建
人工智能·python·深度学习·langchain·大模型·agent·react智能体
MoRanzhi12033 小时前
基于 SciPy 的矩阵运算与线性代数应用详解
人工智能·python·线性代数·算法·数学建模·矩阵·scipy
伊织code3 小时前
Django - DRF
后端·python·django·drf
学习路上_write4 小时前
新版Pycharm添加导入anaconda的python解释器
开发语言·python·pycharm
linuxxx1104 小时前
Django HttpRequest 对象的常用属性
数据库·sqlite
TH88865 小时前
小麦赤霉病监测设备:通过多维度数据采集与智能分析,实现病害的早发现、早预警、早防控
python
Q_Q19632884756 小时前
python+springboot+uniapp基于微信小程序的校园二手闲置二手交易公益系统 二手交易+公益捐赠
spring boot·python·django·flask·uni-app·node.js·php
做运维的阿瑞9 小时前
Python零基础入门:30分钟掌握核心语法与实战应用
开发语言·后端·python·算法·系统架构
Q_Q19632884759 小时前
python+spring boot洪涝灾害应急信息管理系统 灾情上报 预警发布 应急资源调度 灾情图表展示系统
开发语言·spring boot·python·django·flask·node.js·php