博客系统

项目概述

博客系统是一个功能丰富的Web应用程序,它将综合运用前面所学的所有PHP知识。通过开发博客系统,你将学习到更复杂的业务逻辑处理、文件上传、分页、搜索等高级功能。

项目目标

完成本项目后,你将能够:

  • 掌握复杂业务逻辑的处理方法
  • 学会实现文件上传功能
  • 掌握分页显示技术
  • 实现全文搜索功能
  • 学习富文本编辑器的集成
  • 掌握后台管理系统的开发
  • 学会实现缓存机制
  • 了解SEO优化技巧

功能特性

前台功能

  • 文章列表(分页、分类筛选)
  • 文章详情(阅读量统计、评论)
  • 分类和标签系统
  • 搜索功能(全文搜索)
  • 评论系统
  • 用户互动(点赞、收藏)

后台功能

  • 文章管理(发布、编辑、删除)
  • 分类标签管理
  • 评论管理
  • 用户管理
  • 系统设置
  • 数据统计

系统架构

目录结构

blog-system/
├── public/                    # Web根目录
│   ├── index.php            # 入口文件
│   ├── css/
│   │   ├── admin.css         # 后台样式
│   │   ├── blog.css          # 前台样式
│   │   └── editor.css        # 编辑器样式
│   ├── js/
│   │   ├── admin.js          # 后台脚本
│   │   ├── blog.js           # 前台脚本
│   │   └── editor.js         # 编辑器脚本
│   ├── uploads/
│   │   ├── images/           # 图片上传
│   │   └── files/            # 文件上传
│   └── assets/               # 静态资源
├── src/                      # 源代码
│   ├── config/
│   │   ├── database.php      # 数据库配置
│   │   ├── app.php           # 应用配置
│   │   └── constants.php     # 常量定义
│   ├── controllers/
│   │   ├── BaseController.php
│   │   ├── BlogController.php
│   │   ├── AdminController.php
│   │   ├── CommentController.php
│   │   └── UserController.php
│   ├── models/
│   │   ├── Database.php       # 数据库基类
│   │   ├── Blog.php           # 博客模型
│   │   ├── Category.php      # 分类模型
│   │   ├── Tag.php           # 标签模型
│   │   ├── Comment.php       # 评论模型
│   │   ├── User.php          # 用户模型
│   │   └── Upload.php        # 文件上传模型
│   ├── views/
│   │   ├── layouts/
│   │   │   ├── header.php
│   │   │   ├── footer.php
│   │   │   ├── admin_header.php
│   │   │   └── admin_footer.php
│   │   ├── blog/
│   │   │   ├── index.php      # 首页
│   │   │   ├── article.php    # 文章详情
│   │   │   ├── category.php   # 分类页面
│   │   │   ├── search.php     # 搜索页面
│   │   │   └── tag.php        # 标签页面
│   │   └── admin/
│   │       ├── dashboard.php  # 仪表板
│   │       ├── articles.php   # 文章管理
│   │       ├── categories.php # 分类管理
│   │       ├── comments.php   # 评论管理
│   │       ├── users.php      # 用户管理
│   │       └── settings.php   # 系统设置
│   └── helpers/
│       ├── Validator.php     # 验证助手
│       ├── Pagination.php    # 分页助手
│       ├── FileManager.php   # 文件管理
│       ├── Cache.php         # 缓存助手
│       └── SEO.php           # SEO助手
├── database/
│   ├── create_tables.sql      # 建表SQL
│   └── seed_data.sql         # 测试数据
├── cache/                     # 缓存目录
├── logs/                      # 日志目录
└── README.md

数据库设计

1. 创建数据库表

-- database/create_tables.sql

-- 文章表
CREATE TABLE articles (
    id INT PRIMARY KEY AUTO_INCREMENT,
    title VARCHAR(255) NOT NULL,
    slug VARCHAR(255) UNIQUE NOT NULL,
    content LONGTEXT NOT NULL,
    excerpt TEXT,
    featured_image VARCHAR(255),
    author_id INT NOT NULL,
    status ENUM('draft', 'published', 'trash') DEFAULT 'draft',
    comment_status ENUM('open', 'closed') DEFAULT 'open',
    view_count INT DEFAULT 0,
    like_count INT DEFAULT 0,
    is_featured BOOLEAN DEFAULT FALSE,
    seo_title VARCHAR(255),
    seo_description TEXT,
    seo_keywords VARCHAR(255),
    published_at TIMESTAMP NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    FOREIGN KEY (author_id) REFERENCES users(id) ON DELETE CASCADE
);

-- 分类表
CREATE TABLE categories (
    id INT PRIMARY KEY AUTO_INCREMENT,
    name VARCHAR(100) NOT NULL,
    slug VARCHAR(100) UNIQUE NOT NULL,
    description TEXT,
    parent_id INT NULL,
    sort_order INT DEFAULT 0,
    article_count INT DEFAULT 0,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    FOREIGN KEY (parent_id) REFERENCES categories(id) ON DELETE SET NULL
);

-- 标签表
CREATE TABLE tags (
    id INT PRIMARY KEY AUTO_INCREMENT,
    name VARCHAR(100) NOT NULL,
    slug VARCHAR(100) UNIQUE NOT NULL,
    color VARCHAR(7) DEFAULT '#007bff',
    article_count INT DEFAULT 0,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- 文章分类关联表
CREATE TABLE article_categories (
    id INT PRIMARY KEY AUTO_INCREMENT,
    article_id INT NOT NULL,
    category_id INT NOT NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    FOREIGN KEY (article_id) REFERENCES articles(id) ON DELETE CASCADE,
    FOREIGN KEY (category_id) REFERENCES categories(id) ON DELETE CASCADE,
    UNIQUE KEY unique_article_category (article_id, category_id)
);

-- 文章标签关联表
CREATE TABLE article_tags (
    id INT PRIMARY KEY AUTO_INCREMENT,
    article_id INT NOT NULL,
    tag_id INT NOT NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    FOREIGN KEY (article_id) REFERENCES articles(id) ON DELETE CASCADE,
    FOREIGN KEY (tag_id) REFERENCES tags(id) ON DELETE CASCADE,
    UNIQUE KEY unique_article_tag (article_id, tag_id)
);

-- 评论表
CREATE TABLE comments (
    id INT PRIMARY KEY AUTO_INCREMENT,
    article_id INT NOT NULL,
    user_id INT NULL,
    parent_id INT NULL,
    author_name VARCHAR(100),
    author_email VARCHAR(100),
    author_ip VARCHAR(45),
    content TEXT NOT NULL,
    status ENUM('pending', 'approved', 'spam', 'trash') DEFAULT 'pending',
    like_count INT DEFAULT 0,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    FOREIGN KEY (article_id) REFERENCES articles(id) ON DELETE CASCADE,
    FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE SET NULL,
    FOREIGN KEY (parent_id) REFERENCES comments(id) ON DELETE CASCADE
);

-- 用户表(复用用户认证系统的表)
-- 创建索引
CREATE INDEX idx_articles_status ON articles(status);
CREATE INDEX idx_articles_published_at ON articles(published_at);
CREATE INDEX idx_articles_author_id ON articles(author_id);
CREATE INDEX idx_articles_slug ON articles(slug);
CREATE INDEX idx_articles_featured ON articles(is_featured);
CREATE INDEX idx_categories_parent_id ON categories(parent_id);
CREATE INDEX idx_categories_slug ON categories(slug);
CREATE INDEX idx_tags_slug ON tags(slug);
CREATE INDEX idx_comments_article_id ON comments(article_id);
CREATE INDEX idx_comments_status ON comments(status);
CREATE INDEX idx_comments_created_at ON comments(created_at);

-- 创建全文索引(用于搜索)
CREATE FULLTEXT INDEX ft_articles_title ON articles(title);
CREATE FULLTEXT INDEX ft_articles_content ON articles(content);
CREATE FULLTEXT INDEX ft_articles_full ON articles(title, content);

核心代码实现

1. 文章模型 (src/models/Blog.php)

<?php
require_once __DIR__ . '/../config/database.php';

class Blog extends BaseController {

    // 获取文章列表
    public function getArticles($page = 1, $limit = 10, $category = null, $tag = null) {
        $offset = ($page - 1) * $limit;
        $params = [];
        $whereConditions = ["a.status = 'published'"];

        // 添加分类筛选
        if ($category) {
            $whereConditions[] = "ac.category_id = ?";
            $params[] = $category;
        }

        // 添加标签筛选
        if ($tag) {
            $whereConditions[] = "at.tag_id = ?";
            $params[] = $tag;
        }

        $whereClause = "WHERE " . implode(" AND ", $whereConditions);

        $sql = "SELECT DISTINCT a.*, u.username as author_name,
                       GROUP_CONCAT(DISTINCT c.name) as categories,
                       GROUP_CONCAT(DISTINCT t.name) as tags
                FROM articles a
                LEFT JOIN users u ON a.author_id = u.id
                LEFT JOIN article_categories ac ON a.id = ac.article_id
                LEFT JOIN categories c ON ac.category_id = c.id
                LEFT JOIN article_tags at ON a.id = at.article_id
                LEFT JOIN tags t ON at.tag_id = t.id
                {$whereClause}
                GROUP BY a.id
                ORDER BY a.published_at DESC
                LIMIT ? OFFSET ?";

        $params[] = $limit;
        $params[] = $offset;

        $stmt = $this->db->prepare($sql);
        $stmt->execute($params);

        return $stmt->fetchAll();
    }

    // 获取文章总数
    public function getArticlesCount($category = null, $tag = null) {
        $params = [];
        $whereConditions = ["a.status = 'published'"];

        if ($category) {
            $whereConditions[] = "ac.category_id = ?";
            $params[] = $category;
        }

        if ($tag) {
            $whereConditions[] = "at.tag_id = ?";
            $params[] = $tag;
        }

        $whereClause = "WHERE " . implode(" AND ", $whereConditions);

        $sql = "SELECT COUNT(DISTINCT a.id) as count
                FROM articles a
                LEFT JOIN article_categories ac ON a.id = ac.article_id
                LEFT JOIN article_tags at ON a.id = at.article_id
                {$whereClause}";

        $stmt = $this->db->prepare($sql);
        $stmt->execute($params);

        return $stmt->fetch()['count'];
    }

    // 获取单篇文章
    public function getArticleById($id) {
        $sql = "SELECT a.*, u.username as author_name,
                       GROUP_CONCAT(DISTINCT c.name) as categories,
                       GROUP_CONCAT(DISTINCT c.id) as category_ids,
                       GROUP_CONCAT(DISTINCT t.name) as tags,
                       GROUP_CONCAT(DISTINCT t.id) as tag_ids
                FROM articles a
                LEFT JOIN users u ON a.author_id = u.id
                LEFT JOIN article_categories ac ON a.id = ac.article_id
                LEFT JOIN categories c ON ac.category_id = c.id
                LEFT JOIN article_tags at ON a.id = at.article_id
                LEFT JOIN tags t ON at.tag_id = t.id
                WHERE a.id = ?
                GROUP BY a.id";

        $stmt = $this->db->prepare($sql);
        $stmt->execute([$id]);

        $article = $stmt->fetch();

        // 转换分类和标签为数组
        if ($article) {
            $article['categories'] = $article['categories'] ?
                array_combine(
                    explode(',', $article['category_ids']),
                    explode(',', $article['categories'])
                ) : [];
            $article['tags'] = $article['tags'] ?
                array_combine(
                    explode(',', $article['tag_ids']),
                    explode(',', $article['tags'])
                ) : [];
        }

        return $article;
    }

    // 通过slug获取文章
    public function getArticleBySlug($slug) {
        $sql = "SELECT a.*, u.username as author_name
                FROM articles a
                LEFT JOIN users u ON a.author_id = u.id
                WHERE a.slug = ? AND a.status = 'published'";

        $stmt = $this->db->prepare($sql);
        $stmt->execute([$slug]);

        return $stmt->fetch();
    }

    // 创建文章
    public function createArticle($articleData) {
        // 生成唯一的slug
        $slug = $this->generateUniqueSlug($articleData['title']);

        $sql = "INSERT INTO articles (title, slug, content, excerpt, featured_image,
                                       author_id, status, seo_title, seo_description,
                                       seo_keywords, is_featured, published_at, created_at)
                VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NOW())";

        $params = [
            $articleData['title'],
            $slug,
            $articleData['content'],
            $articleData['excerpt'],
            $articleData['featured_image'],
            $articleData['author_id'],
            $articleData['status'],
            $articleData['seo_title'],
            $articleData['seo_description'],
            $articleData['seo_keywords'],
            $articleData['is_featured'] ?? false,
            $articleData['published_at'] ?? null
        ];

        $stmt = $this->db->prepare($sql);
        $stmt->execute($params);

        $articleId = $this->db->lastInsertId();

        // 保存分类和标签
        if (!empty($articleData['categories'])) {
            $this->saveArticleCategories($articleId, $articleData['categories']);
        }

        if (!empty($articleData['tags'])) {
            $this->saveArticleTags($articleId, $articleData['tags']);
        }

        return $articleId;
    }

    // 更新文章
    public function updateArticle($id, $articleData) {
        // 如果标题改变,重新生成slug
        if (isset($articleData['title']) && $articleData['title']) {
            $articleData['slug'] = $this->generateUniqueSlug($articleData['title'], $id);
        }

        $fields = [];
        $params = [];

        $updateFields = [
            'title', 'slug', 'content', 'excerpt', 'featured_image',
            'status', 'seo_title', 'seo_description',
            'seo_keywords', 'is_featured'
        ];

        foreach ($updateFields as $field) {
            if (isset($articleData[$field])) {
                $fields[] = "{$field} = ?";
                $params[] = $articleData[$field];
            }
        }

        if (isset($articleData['published_at'])) {
            $fields[] = "published_at = ?";
            $params[] = $articleData['published_at'];
        }

        $fields[] = "updated_at = NOW()";
        $params[] = $id;

        $sql = "UPDATE articles SET " . implode(', ', $fields) . " WHERE id = ?";
        $stmt = $this->db->prepare($sql);
        $result = $stmt->execute($params);

        // 更新分类和标签
        if (isset($articleData['categories'])) {
            $this->saveArticleCategories($id, $articleData['categories']);
        }

        if (isset($articleData['tags'])) {
            $this->saveArticleTags($id, $articleData['tags']);
        }

        return $result;
    }

    // 删除文章
    public function deleteArticle($id) {
        // 开始事务
        $this->db->beginTransaction();

        try {
            // 删除文章分类关联
            $this->db->prepare("DELETE FROM article_categories WHERE article_id = ?")
                 ->execute([$id]);

            // 删除文章标签关联
            $this->db->prepare("DELETE FROM article_tags WHERE article_id = ?")
                 ->execute([$id]);

            // 删除文章
            $this->db->prepare("DELETE FROM articles WHERE id = ?")
                 ->execute([$id]);

            $this->db->commit();
            return true;

        } catch (Exception $e) {
            $this->db->rollBack();
            return false;
        }
    }

    // 搜索文章
    public function searchArticles($keyword, $page = 1, $limit = 10) {
        $offset = ($page - 1) * $limit;

        // 使用全文搜索
        $sql = "SELECT a.*, u.username as author_name,
                       MATCH(a.title, a.content) AGAINST(?) as score
                FROM articles a
                LEFT JOIN users u ON a.author_id = u.id
                WHERE a.status = 'published'
                AND MATCH(a.title, a.content) AGAINST(? IN NATURAL LANGUAGE MODE)
                ORDER BY score DESC, a.published_at DESC
                LIMIT ? OFFSET ?";

        $stmt = $this->db->prepare($sql);
        $stmt->execute([$keyword, $keyword, $limit, $offset]);

        return $stmt->fetchAll();
    }

    // 获取搜索结果总数
    public function getSearchCount($keyword) {
        $sql = "SELECT COUNT(*) as count
                FROM articles a
                WHERE a.status = 'published'
                AND MATCH(a.title, a.content) AGAINST(? IN NATURAL LANGUAGE MODE)";

        $stmt = $this->db->prepare($sql);
        $stmt->execute([$keyword]);

        return $stmt->fetch()['count'];
    }

    // 增加阅读量
    public function incrementViewCount($id) {
        $sql = "UPDATE articles SET view_count = view_count + 1 WHERE id = ?";
        $stmt = $this->db->prepare($sql);
        return $stmt->execute([$id]);
    }

    // 点赞/取消点赞
    public function toggleLike($articleId, $userId) {
        // 检查是否已经点赞
        $sql = "SELECT * FROM article_likes WHERE article_id = ? AND user_id = ?";
        $stmt = $this->db->prepare($sql);
        $stmt->execute([$articleId, $userId]);

        if ($stmt->fetch()) {
            // 取消点赞
            $this->db->prepare("DELETE FROM article_likes WHERE article_id = ? AND user_id = ?")
                 ->execute([$articleId, $userId]);

            $this->db->prepare("UPDATE articles SET like_count = like_count - 1 WHERE id = ?")
                 ->execute([$articleId]);

            return false;
        } else {
            // 添加点赞
            $this->db->prepare("INSERT INTO article_likes (article_id, user_id, created_at) VALUES (?, ?, NOW())")
                 ->execute([$articleId, $userId]);

            $this->db->prepare("UPDATE articles SET like_count = like_count + 1 WHERE id = ?")
                 ->execute([$articleId]);

            return true;
        }
    }

    // 生成唯一的slug
    private function generateUniqueSlug($title, $id = null) {
        $slug = $this->createSlug($title);
        $originalSlug = $slug;
        $count = 1;

        // 检查slug是否唯一
        while (true) {
            $sql = "SELECT id FROM articles WHERE slug = ?";
            $params = [$slug];

            if ($id) {
                $sql .= " AND id != ?";
                $params[] = $id;
            }

            $stmt = $this->db->prepare($sql);
            $stmt->execute($params);

            if (!$stmt->fetch()) {
                return $slug;
            }

            $slug = $originalSlug . '-' . $count;
            $count++;
        }
    }

    // 创建slug
    private function createSlug($string) {
        // 转换为小写
        $string = strtolower($string);

        // 替换特殊字符
        $string = preg_replace('/[^a-z0-9\s-]/', '', $string);

        // 替换多个空格为单个连字符
        $string = preg_replace('/[\s-]+/', '-', $string);

        // 删除首尾的连字符
        return trim($string, '-');
    }

    // 保存文章分类
    private function saveArticleCategories($articleId, $categories) {
        // 删除现有的分类关联
        $this->db->prepare("DELETE FROM article_categories WHERE article_id = ?")
             ->execute([$articleId]);

        // 添加新的分类关联
        $sql = "INSERT INTO article_categories (article_id, category_id, created_at) VALUES (?, ?, NOW())";
        $stmt = $this->db->prepare($sql);

        foreach ($categories as $categoryId) {
            $stmt->execute([$articleId, $categoryId]);
        }
    }

    // 保存文章标签
    private function saveArticleTags($articleId, $tags) {
        // 删除现有的标签关联
        $this->db->prepare("DELETE FROM article_tags WHERE article_id = ?")
             ->execute([$articleId]);

        // 添加新的标签关联
        $sql = "INSERT INTO article_tags (article_id, tag_id, created_at) VALUES (?, ?, NOW())";
        $stmt = $this->db->prepare($sql);

        foreach ($tags as $tagId) {
            $stmt->execute([$articleId, $tagId]);
        }
    }
}

2. 博客控制器 (src/controllers/BlogController.php)

<?php
require_once 'BaseController.php';
require_once __DIR__ . '/../models/Blog.php';
require_once __DIR__ . '/../models/Category.php';
require_once __DIR__ . '/../models/Tag.php';
require_once __DIR__ . '/../models/Comment.php';
require_once __DIR__ . '/../helpers/Pagination.php';

class BlogController extends BaseController {
    private $blog;
    private $category;
    private $tag;
    private $comment;

    public function __construct() {
        parent::__construct();
        $this->blog = new Blog();
        $this->category = new Category();
        $this->tag = new Tag();
        $this->comment = new Comment();
    }

    // 首页 - 文章列表
    public function index() {
        $page = $this->getGet('page', 1);
        $category = $this->getGet('category');
        $tag = $this->getGet('tag');

        $articles = $this->blog->getArticles($page, 10, $category, $tag);
        $totalArticles = $this->blog->getArticlesCount($category, $tag);

        $pagination = new Pagination($totalArticles, $page, 10, 5);
        $pagination->setUrl($_SERVER['REQUEST_URI']);

        $this->render('blog/index.php', [
            'articles' => $articles,
            'pagination' => $pagination,
            'categories' => $this->category->getAll(),
            'popularTags' => $this->tag->getPopular(10)
        ]);
    }

    // 文章详情
    public function article() {
        $slug = $this->getGet('slug');

        if (!$slug) {
            $this->render404();
        }

        $article = $this->blog->getArticleBySlug($slug);

        if (!$article) {
            $this->render404();
        }

        // 增加阅读量
        $this->blog->incrementViewCount($article['id']);

        // 获取评论
        $comments = $this->comment->getApprovedComments($article['id']);
        $relatedArticles = $this->getRelatedArticles($article);

        $this->render('blog/article.php', [
            'article' => $article,
            'comments' => $comments,
            'relatedArticles' => $relatedArticles
        ]);
    }

    // 分类页面
    public function category() {
        $slug = $this->getGet('slug');
        $page = $this->getGet('page', 1);

        $category = $this->category->getBySlug($slug);
        if (!$category) {
            $this->render404();
        }

        $articles = $this->blog->getArticles($page, 10, $category['id']);
        $totalArticles = $this->blog->getArticlesCount($category['id']);

        $pagination = new Pagination($totalArticles, $page, 10);
        $pagination->setUrl($_SERVER['REQUEST_URI']);

        $this->render('blog/category.php', [
            'category' => $category,
            'articles' => $articles,
            'pagination' => $pagination
        ]);
    }

    // 标签页面
    public function tag() {
        $slug = $this->getGet('slug');
        $page = $this->getGet('page', 1);

        $tag = $this->tag->getBySlug($slug);
        if (!$tag) {
            $this->render404();
        }

        $articles = $this->blog->getArticles($page, 10, null, $tag['id']);
        $totalArticles = $this->blog->getArticlesCount(null, $tag['id']);

        $pagination = new Pagination($totalArticles, $page, 10);
        $pagination->setUrl($_SERVER['REQUEST_URI']);

        $this->render('blog/tag.php', [
            'tag' => $tag,
            'articles' => $articles,
            'pagination' => $pagination
        ]);
    }

    // 搜索页面
    public function search() {
        $keyword = trim($this->getGet('q'));
        $page = $this->getGet('page', 1);

        if (!$keyword) {
            $this->redirect('index.php');
        }

        $articles = $this->blog->searchArticles($keyword, $page, 10);
        $totalResults = $this->blog->getSearchCount($keyword);

        $pagination = new Pagination($totalResults, $page, 10);
        $pagination->setUrl($_SERVER['REQUEST_URI']);

        $this->render('blog/search.php', [
            'keyword' => $keyword,
            'articles' => $articles,
            'totalResults' => $totalResults,
            'pagination' => $pagination
        ]);
    }

    // 提交评论
    public function submitComment() {
        if (!$this->isPost()) {
            $this->json(['success' => false, 'message' => '无效的请求']);
        }

        $articleId = $this->getPost('article_id');
        $parentId = $this->getPost('parent_id');
        $content = trim($this->getPost('content'));
        $authorName = trim($this->getPost('author_name'));
        $authorEmail = trim($this->getPost('author_email'));

        // 验证数据
        if (!$articleId || !$content) {
            $this->json(['success' => false, 'message' => '请填写必填字段']);
        }

        if (!$authorName || !$authorEmail) {
            $this->json(['success' => false, 'message' => '请填写姓名和邮箱']);
        }

        if (!filter_var($authorEmail, FILTER_VALIDATE_EMAIL)) {
            $this->json(['success' => false, 'message' => '邮箱格式错误']);
        }

        // 检查文章是否存在
        if (!$this->blog->getArticleById($articleId)) {
            $this->json(['success' => false, 'message' => '文章不存在']);
        }

        // 防止重复提交
        if (isset($_SESSION['last_comment_time']) && time() - $_SESSION['last_comment_time'] < 10) {
            $this->json(['success' => false, 'message' => '请稍后再试']);
        }

        // 验证码验证(如果需要)
        if (isset($_POST['captcha'])) {
            if (!$this->validateCaptcha($_POST['captcha'])) {
                $this->json(['success' => false, 'message' => '验证码错误']);
            }
        }

        // 提交评论
        $commentId = $this->comment->create([
            'article_id' => $articleId,
            'user_id' => $this->getSession('user_id'),
            'parent_id' => $parentId ?: null,
            'author_name' => $authorName,
            'author_email' => $authorEmail,
            'content' => $content,
            'author_ip' => $_SERVER['REMOTE_ADDR'],
            'status' => 'pending' // 评论需要审核
        ]);

        if ($commentId) {
            $_SESSION['last_comment_time'] = time();
            $this->json([
                'success' => true,
                'message' => '评论提交成功,等待审核'
            ]);
        } else {
            $this->json(['success' => false, 'message' => '评论提交失败']);
        }
    }

    // AJAX点赞
    public function like() {
        if (!$this->isPost()) {
            $this->json(['success' => false, 'message' => '无效的请求']);
        }

        $articleId = $this->getPost('article_id');
        $userId = $this->getSession('user_id');

        if (!$articleId) {
            $this->json(['success' => false, 'message' => '参数错误']);
        }

        if (!$userId) {
            $this->json(['success' => false, 'message' => '请先登录']);
        }

        $isLiked = $this->blog->toggleLike($articleId, $userId);
        $article = $this->blog->getArticleById($articleId);

        $this->json([
            'success' => true,
            'liked' => $isLiked,
            'likeCount' => $article['like_count'] ?? 0
        ]);
    }

    // 获取相关文章
    private function getRelatedArticles($article, $limit = 4) {
        // 基于标签查找相关文章
        $tagIds = array_keys($article['tags']);
        if (empty($tagIds)) {
            return [];
        }

        $placeholders = implode(',', array_fill(0, count($tagIds), '?'));
        $sql = "SELECT DISTINCT a.id, a.title, a.slug, a.excerpt, a.featured_image,
                       a.published_at, a.view_count
                FROM articles a
                LEFT JOIN article_tags at ON a.id = at.article_id
                WHERE a.id != ? AND a.status = 'published'
                AND at.tag_id IN ({$placeholders})
                ORDER BY a.published_at DESC
                LIMIT ?";

        $params = array_merge([$article['id']], $tagIds, [$limit]);
        $stmt = $this->db->prepare($sql);
        $stmt->execute($params);

        return $stmt->fetchAll();
    }

    // 验证码验证
    private function validateCaptcha($captcha) {
        // 简单的验证码验证
        // 实际项目中可以使用 reCAPTCHA
        if (isset($_SESSION['captcha'])) {
            return hash_equals($_SESSION['captcha'], $captcha);
        }
        return true;
    }

    // 404页面
    private function render404() {
        header('HTTP/1.0 404 Not Found');
        $this->render('blog/404.php');
        exit;
    }
}

3. 分页助手 (src/helpers/Pagination.php)

<?php
class Pagination {
    private $totalItems;
    private $currentPage;
    private $itemsPerPage;
    private $maxVisiblePages;
    private $url;
    private $totalPages;

    public function __construct($totalItems, $currentPage = 1, $itemsPerPage = 10, $maxVisiblePages = 7) {
        $this->totalItems = $totalItems;
        $this->currentPage = max(1, $currentPage);
        $this->itemsPerPage = $itemsPerPage;
        $this->maxVisiblePages = $maxVisiblePages;
        $this->totalPages = ceil($totalItems / $itemsPerPage);
    }

    public function setUrl($url) {
        // 移除现有的分页参数
        $url = preg_replace('/[?&]page=\d+/', '', $url);

        // 添加分隔符
        $separator = (strpos($url, '?') === false) ? '?' : '&';
        $this->url = $url . $separator . 'page=';
    }

    public function createLinks() {
        if ($this->totalPages <= 1) {
            return '';
        }

        $links = '<nav aria-label="Page navigation"><ul class="pagination justify-content-center">';

        // 上一页
        if ($this->currentPage > 1) {
            $links .= '<li class="page-item"><a class="page-link" href="' . $this->url . ($this->currentPage - 1) . '">上一页</a></li>';
        } else {
            $links .= '<li class="page-item disabled"><span class="page-link">上一页</span></li>';
        }

        // 页码
        $startPage = max(1, $this->currentPage - floor($this->maxVisiblePages / 2));
        $endPage = min($this->totalPages, $startPage + $this->maxVisiblePages - 1);

        // 调整开始页
        if ($endPage - $startPage < $this->maxVisiblePages - 1) {
            $startPage = max(1, $endPage - $this->maxVisiblePages + 1);
        }

        // 第一页
        if ($startPage > 1) {
            $links .= '<li class="page-item"><a class="page-link" href="' . $this->url . '1">1</a></li>';
            if ($startPage > 2) {
                $links .= '<li class="page-item disabled"><span class="page-link">...</span></li>';
            }
        }

        // 中间页码
        for ($i = $startPage; $i <= $endPage; $i++) {
            if ($i == $this->currentPage) {
                $links .= '<li class="page-item active"><span class="page-link">' . $i . '</span></li>';
            } else {
                $links .= '<li class="page-item"><a class="page-link" href="' . $this->url . $i . '">' . $i . '</a></li>';
            }
        }

        // 最后一页
        if ($endPage < $this->totalPages) {
            if ($endPage < $this->totalPages - 1) {
                $links .= '<li class="page-item disabled"><span class="page-link">...</span></li>';
            }
            $links .= '<li class="page-item"><a class="page-link" href="' . $this->url . $this->totalPages . '">' . $this->totalPages . '</a></li>';
        }

        // 下一页
        if ($this->currentPage < $this->totalPages) {
            $links .= '<li class="page-item"><a class="page-link" href="' . $this->url . ($this->currentPage + 1) . '">下一页</a></li>';
        } else {
            $links .= '<li class="page-item disabled"><span class="page-link">下一页</span></li>';
        }

        $links .= '</ul></nav>';

        return $links;
    }

    public function getInfo() {
        $start = ($this->currentPage - 1) * $this->itemsPerPage + 1;
        $end = min($this->totalItems, $start + $this->itemsPerPage - 1);

        return [
            'total' => $this->totalItems,
            'current' => $this->currentPage,
            'start' => $this->totalItems > 0 ? $start : 0,
            'end' => $end,
            'pages' => $this->totalPages,
            'items_per_page' => $this->itemsPerPage
        ];
    }
}

4. 文章管理视图示例 (src/views/admin/articles.php)

<div class="container-fluid">
    <div class="row">
        <div class="col-12">
            <div class="card">
                <div class="card-header d-flex justify-content-between align-items-center">
                    <h5 class="mb-0">文章管理</h5>
                    <a href="article-edit.php" class="btn btn-primary">
                        <i class="fas fa-plus"></i> 新建文章
                    </a>
                </div>
                <div class="card-body">
                    <!-- 搜索和筛选 -->
                    <div class="row mb-3">
                        <div class="col-md-6">
                            <div class="input-group">
                                <input type="text" class="form-control" id="searchInput" placeholder="搜索文章...">
                                <button class="btn btn-outline-secondary" type="button" id="searchBtn">
                                    <i class="fas fa-search"></i>
                                </button>
                            </div>
                        </div>
                        <div class="col-md-3">
                            <select class="form-select" id="statusFilter">
                                <option value="">所有状态</option>
                                <option value="published">已发布</option>
                                <option value="draft">草稿</option>
                                <option value="trash">回收站</option>
                            </select>
                        </div>
                        <div class="col-md-3">
                            <select class="form-select" id="categoryFilter">
                                <option value="">所有分类</option>
                                <?php foreach ($categories as $category): ?>
                                    <option value="<?php echo $category['id']; ?>">
                                        <?php echo htmlspecialchars($category['name']); ?>
                                    </option>
                                <?php endforeach; ?>
                            </select>
                        </div>
                    </div>

                    <!-- 文章表格 -->
                    <div class="table-responsive">
                        <table class="table table-hover">
                            <thead>
                                <tr>
                                    <th><input type="checkbox" id="selectAll"></th>
                                    <th>标题</th>
                                    <th>作者</th>
                                    <th>分类</th>
                                    <th>状态</th>
                                    <th>发布时间</th>
                                    <th>阅读量</th>
                                    <th>操作</th>
                                </tr>
                            </thead>
                            <tbody id="articlesTable">
                                <?php foreach ($articles as $article): ?>
                                    <tr>
                                        <td><input type="checkbox" name="article_ids[]" value="<?php echo $article['id']; ?>"></td>
                                        <td>
                                            <a href="article-edit.php?id=<?php echo $article['id']; ?>">
                                                <?php echo htmlspecialchars($article['title']); ?>
                                            </a>
                                            <?php if ($article['is_featured']): ?>
                                                <span class="badge bg-warning">推荐</span>
                                            <?php endif; ?>
                                        </td>
                                        <td><?php echo htmlspecialchars($article['author_name']); ?></td>
                                        <td><?php echo $article['categories']; ?></td>
                                        <td>
                                            <?php
                                            $statusClass = [
                                                'draft' => 'secondary',
                                                'published' => 'success',
                                                'trash' => 'danger'
                                            ];
                                            $statusText = [
                                                'draft' => '草稿',
                                                'published' => '已发布',
                                                'trash' => '回收站'
                                            ];
                                            ?>
                                            <span class="badge bg-<?php echo $statusClass[$article['status']]; ?>">
                                                <?php echo $statusText[$article['status']]; ?>
                                            </span>
                                        </td>
                                        <td><?php echo date('Y-m-d', strtotime($article['published_at'] ?? $article['created_at'])); ?></td>
                                        <td><?php echo number_format($article['view_count']); ?></td>
                                        <td>
                                            <div class="btn-group btn-group-sm">
                                                <a href="article-edit.php?id=<?php echo $article['id']; ?>"
                                                   class="btn btn-outline-primary">
                                                    <i class="fas fa-edit"></i>
                                                </a>
                                                <a href="article-view.php?id=<?php echo $article['id']; ?>"
                                                   class="btn btn-outline-info" target="_blank">
                                                    <i class="fas fa-eye"></i>
                                                </a>
                                                <button type="button" class="btn btn-outline-danger delete-btn"
                                                        data-id="<?php echo $article['id']; ?>">
                                                    <i class="fas fa-trash"></i>
                                                </button>
                                            </div>
                                        </td>
                                    </tr>
                                <?php endforeach; ?>
                            </tbody>
                        </table>
                    </div>

                    <!-- 分页 -->
                    <?php echo $pagination->createLinks(); ?>
                </div>
            </div>
        </div>
    </div>
</div>

<!-- 批量操作模态框 -->
<div class="modal fade" id="batchModal" tabindex="-1">
    <div class="modal-dialog">
        <div class="modal-content">
            <div class="modal-header">
                <h5 class="modal-title">批量操作</h5>
                <button type="button" class="btn-close" data-bs-dismiss="modal"></button>
            </div>
            <div class="modal-body">
                <p>请选择要执行的操作:</p>
                <div class="d-grid gap-2">
                    <button type="button" class="btn btn-outline-primary" id="batchPublish">发布选中</button>
                    <button type="button" class="btn btn-outline-secondary" id="batchDraft">移至草稿</button>
                    <button type="button" class="btn btn-outline-danger" id="batchTrash">移至回收站</button>
                </div>
            </div>
        </div>
    </div>
</div>

<script>
document.addEventListener('DOMContentLoaded', function() {
    // 全选/取消全选
    const selectAll = document.getElementById('selectAll');
    const checkboxes = document.querySelectorAll('input[name="article_ids[]"]');

    selectAll?.addEventListener('change', function() {
        checkboxes.forEach(checkbox => {
            checkbox.checked = this.checked;
        });
    });

    // 搜索功能
    const searchBtn = document.getElementById('searchBtn');
    const searchInput = document.getElementById('searchInput');

    function performSearch() {
        const keyword = searchInput.value;
        const status = document.getElementById('statusFilter').value;
        const category = document.getElementById('categoryFilter').value;

        const params = new URLSearchParams();
        if (keyword) params.append('search', keyword);
        if (status) params.append('status', status);
        if (category) params.append('category', category);

        window.location.href = 'articles.php?' + params.toString();
    }

    searchBtn?.addEventListener('click', performSearch);
    searchInput?.addEventListener('keypress', function(e) {
        if (e.key === 'Enter') {
            performSearch();
        }
    });

    // 状态和分类筛选
    document.getElementById('statusFilter')?.addEventListener('change', performSearch);
    document.getElementById('categoryFilter')?.addEventListener('change', performSearch);

    // 删除按钮
    document.querySelectorAll('.delete-btn').forEach(btn => {
        btn.addEventListener('click', function() {
            const articleId = this.dataset.id;
            if (confirm('确定要删除这篇文章吗?')) {
                fetch('api/article-delete.php', {
                    method: 'POST',
                    headers: {
                        'Content-Type': 'application/json',
                    },
                    body: JSON.stringify({ id: articleId })
                })
                .then(response => response.json())
                .then(data => {
                    if (data.success) {
                        location.reload();
                    } else {
                        alert('删除失败:' + data.message);
                    }
                });
            }
        });
    });
});
</script>

5. 富文本编辑器集成

编辑器配置 (public/js/editor.js)

// 使用TinyMCE富文本编辑器
tinymce.init({
    selector: '#content',
    height: 500,
    theme: 'silver',
    language: 'zh_CN',
    plugins: [
        'advlist', 'autolink', 'lists', 'link', 'image', 'charmap', 'preview',
        'anchor', 'searchreplace', 'visualblocks', 'code', 'fullscreen',
        'insertdatetime', 'media', 'table', 'help', 'wordcount',
        'codesample', 'emoticons'
    ],
    toolbar: 'undo redo | formatselect | bold italic | alignleft aligncenter alignright alignjustify | ' +
             'bullist numlist outdent indent | link image | preview media | ' +
             'forecolor backcolor emoticons | help codesample',
    menubar: 'file edit view insert format tools table help',
    content_style: `
        body { font-family: Arial, sans-serif; font-size: 14px; }
        img { max-width: 100%; height: auto; }
        pre { background: #f4f4f4; border: 1px solid #ddd; padding: 10px; border-radius: 4px; }
        code { background: #f4f4f4; padding: 2px 4px; border-radius: 2px; }
    `,
    image_advtab: true,
    image_uploadtab: true,
    automatic_uploads: true,
    images_upload_url: 'api/upload-image.php',
    images_upload_handler: function(blobInfo, success, failure) {
        const xhr = new XMLHttpRequest();
        xhr.withCredentials = false;
        xhr.open('POST', 'api/upload-image.php');
        xhr.onload = function() {
            if (xhr.status === 200) {
                const json = JSON.parse(xhr.responseText);
                success(json.location);
            } else {
                failure('Image upload failed: ' + xhr.statusText);
            }
        };
        xhr.send(blobInfo.blob());
    },
    // 自定义样式
    style_formats: [
        {title: 'Heading 2', block: 'h2', classes: 'text-primary'},
        {title: 'Heading 3', block: 'h3', classes: 'text-secondary'},
        {title: 'Button', inline: 'span', classes: 'btn btn-primary'},
        {title: 'Badge', inline: 'span', classes: 'badge bg-primary'}
    ]
});

// 字数统计
function updateWordCount() {
    const content = tinymce.get('content').getContent();
    const plainText = content.replace(/<[^>]*>/g, '').replace(/\s+/g, ' ').trim();
    const wordCount = plainText ? plainText.split(' ').length : 0;
    document.getElementById('wordCount').textContent = wordCount + ' 字';
}

// 自动保存功能
let autoSaveTimer;
function startAutoSave() {
    if (autoSaveTimer) {
        clearInterval(autoSaveTimer);
    }

    autoSaveTimer = setInterval(function() {
        saveDraft();
    }, 30000); // 30秒自动保存一次
}

// 保存草稿
function saveDraft() {
    const content = tinymce.get('content').getContent();
    const title = document.getElementById('title').value;

    if (!title || !content) return;

    fetch('api/save-draft.php', {
        method: 'POST',
        headers: {
            'Content-Type': 'application/json',
        },
        body: JSON.stringify({
            title: title,
            content: content,
            id: document.getElementById('article_id')?.value
        })
    })
    .then(response => response.json())
    .then(data => {
        if (data.success) {
            document.getElementById('saveStatus').textContent = '已自动保存';
            setTimeout(() => {
                document.getElementById('saveStatus').textContent = '';
            }, 3000);
        }
    });
}

后台管理系统

管理员仪表板 (src/views/admin/dashboard.php)

<div class="container-fluid">
    <!-- 统计卡片 -->
    <div class="row mb-4">
        <div class="col-xl-3 col-md-6 mb-4">
            <div class="card border-left-primary shadow h-100 py-2">
                <div class="card-body">
                    <div class="row no-gutters align-items-center">
                        <div class="col mr-2">
                            <div class="text-xs font-weight-bold text-primary text-uppercase mb-1">
                                总文章数
                            </div>
                            <div class="h5 mb-0 font-weight-bold text-gray-800">
                                <?php echo $stats['total_articles']; ?>
                            </div>
                        </div>
                        <div class="col-auto">
                            <i class="fas fa-file-alt fa-2x text-gray-300"></i>
                        </div>
                    </div>
                </div>
            </div>
        </div>

        <div class="col-xl-3 col-md-6 mb-4">
            <div class="card border-left-success shadow h-100 py-2">
                <div class="card-body">
                    <div class="row no-gutters align-items-center">
                        <div class="col mr-2">
                            <div class="text-xs font-weight-bold text-success text-uppercase mb-1">
                                已发布
                            </div>
                            <div class="h5 mb-0 font-weight-bold text-gray-800">
                                <?php echo $stats['published_articles']; ?>
                            </div>
                        </div>
                        <div class="col-auto">
                            <i class="fas fa-check-circle fa-2x text-gray-300"></i>
                        </div>
                    </div>
                </div>
            </div>
        </div>

        <div class="col-xl-3 col-md-6 mb-4">
            <div class="card border-left-info shadow h-100 py-2">
                <div class="card-body">
                    <div class="row no-gutters align-items-center">
                        <div class="col mr-2">
                            <div class="text-xs font-weight-bold text-info text-uppercase mb-1">
                                总评论数
                            </div>
                            <div class="h5 mb-0 font-weight-bold text-gray-800">
                                <?php echo $stats['total_comments']; ?>
                            </div>
                        </div>
                        <div class="col-auto">
                            <i class="fas fa-comments fa-2x text-gray-300"></i>
                        </div>
                    </div>
                </div>
            </div>
        </div>

        <div class="col-xl-3 col-md-6 mb-4">
            <div class="card border-left-warning shadow h-100 py-2">
                <div class="card-body">
                    <div class="row no-gutters align-items-center">
                        <div class="col mr-2">
                            <div class="text-xs font-weight-bold text-warning text-uppercase mb-1">
                                总用户数
                            </div>
                            <div class="h5 mb-0 font-weight-bold text-gray-800">
                                <?php echo $stats['total_users']; ?>
                            </div>
                        </div>
                        <div class="col-auto">
                            <i class="fas fa-users fa-2x text-gray-300"></i>
                        </div>
                    </div>
                </div>
            </div>
        </div>
    </div>

    <!-- 图表区域 -->
    <div class="row mb-4">
        <div class="col-xl-8 col-lg-7">
            <div class="card shadow mb-4">
                <div class="card-header py-3 d-flex flex-row align-items-center justify-content-between">
                    <h6 class="m-0 font-weight-bold text-primary">文章发布趋势</h6>
                </div>
                <div class="card-body">
                    <div class="chart-area">
                        <canvas id="articleChart"></canvas>
                    </div>
                </div>
            </div>
        </div>

        <div class="col-xl-4 col-lg-5">
            <div class="card shadow mb-4">
                <div class="card-header py-3">
                    <h6 class="m-0 font-weight-bold text-primary">文章分类分布</h6>
                </div>
                <div class="card-body">
                    <div class="chart-pie pt-4 pb-2">
                        <canvas id="categoryChart"></canvas>
                    </div>
                </div>
            </div>
        </div>
    </div>

    <!-- 最近文章和评论 -->
    <div class="row">
        <div class="col-lg-6 mb-4">
            <div class="card shadow">
                <div class="card-header py-3">
                    <h6 class="m-0 font-weight-bold text-primary">最近文章</h6>
                </div>
                <div class="card-body">
                    <div class="table-responsive">
                        <table class="table table-sm">
                            <thead>
                                <tr>
                                    <th>标题</th>
                                    <th>作者</th>
                                    <th>状态</th>
                                    <th>时间</th>
                                </tr>
                            </thead>
                            <tbody>
                                <?php foreach ($recentArticles as $article): ?>
                                    <tr>
                                        <td>
                                            <a href="article-edit.php?id=<?php echo $article['id']; ?>">
                                                <?php echo htmlspecialchars(substr($article['title'], 0, 30)); ?>...
                                            </a>
                                        </td>
                                        <td><?php echo htmlspecialchars($article['author_name']); ?></td>
                                        <td>
                                            <span class="badge bg-<?php echo $article['status'] === 'published' ? 'success' : 'secondary'; ?>">
                                                <?php echo $article['status']; ?>
                                            </span>
                                        </td>
                                        <td><?php echo date('m-d', strtotime($article['created_at'])); ?></td>
                                    </tr>
                                <?php endforeach; ?>
                            </tbody>
                        </table>
                    </div>
                </div>
            </div>
        </div>

        <div class="col-lg-6 mb-4">
            <div class="card shadow">
                <div class="card-header py-3">
                    <h6 class="m-0 font-weight-bold text-primary">待审核评论</h6>
                </div>
                <div class="card-body">
                    <?php foreach ($pendingComments as $comment): ?>
                        <div class="mb-3 p-3 border rounded">
                            <div class="d-flex justify-content-between">
                                <strong><?php echo htmlspecialchars($comment['author_name']); ?></strong>
                                <small class="text-muted"><?php echo date('m-d H:i', strtotime($comment['created_at'])); ?></small>
                            </div>
                            <p class="mb-2"><?php echo htmlspecialchars(substr($comment['content'], 0, 100)); ?>...</p>
                            <div class="d-flex gap-2">
                                <a href="comment-edit.php?id=<?php echo $comment['id']; ?>" class="btn btn-sm btn-outline-primary">审核</a>
                                <a href="#" class="btn btn-sm btn-outline-danger delete-comment" data-id="<?php echo $comment['id']; ?>">删除</a>
                            </div>
                        </div>
                    <?php endforeach; ?>

                    <?php if (empty($pendingComments)): ?>
                        <p class="text-muted">暂无待审核评论</p>
                    <?php endif; ?>
                </div>
            </div>
        </div>
    </div>
</div>

<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script>
// 文章发布趋势图
const articleCtx = document.getElementById('articleChart').getContext('2d');
new Chart(articleCtx, {
    type: 'line',
    data: {
        labels: <?php echo json_encode($stats['article_trend']['labels']); ?>,
        datasets: [{
            label: '文章数量',
            data: <?php echo json_encode($stats['article_trend']['data']); ?>,
            borderColor: 'rgb(75, 192, 192)',
            backgroundColor: 'rgba(75, 192, 192, 0.2)',
            tension: 0.1
        }]
    },
    options: {
        responsive: true,
        scales: {
            y: {
                beginAtZero: true
            }
        }
    }
});

// 分类分布饼图
const categoryCtx = document.getElementById('categoryChart').getContext('2d');
new Chart(categoryCtx, {
    type: 'doughnut',
    data: {
        labels: <?php echo json_encode($stats['category_distribution']['labels']); ?>,
        datasets: [{
            data: <?php echo json_encode($stats['category_distribution']['data']); ?>,
            backgroundColor: [
                '#4e73df',
                '#1cc88a',
                '#36b9cc',
                '#f6c23e',
                '#e74a3b',
                '#858796',
                '#5a5c69'
            ]
        }]
    },
    options: {
        responsive: true,
        plugins: {
            legend: {
                position: 'bottom'
            }
        }
    }
});
</script>

性能优化

1. 缓存实现 (src/helpers/Cache.php)

<?php
class Cache {
    private $cacheDir;
    private $defaultExpire = 3600; // 1小时

    public function __construct($cacheDir = 'cache/') {
        $this->cacheDir = $cacheDir;
        if (!is_dir($this->cacheDir)) {
            mkdir($this->cacheDir, 0777, true);
        }
    }

    // 设置缓存
    public function set($key, $data, $expire = null) {
        $expire = $expire ?? $this->defaultExpire;
        $filename = $this->getFilename($key);

        $cacheData = [
            'data' => $data,
            'expire' => time() + $expire,
            'created' => time()
        ];

        file_put_contents($filename, serialize($cacheData), LOCK_EX);
        return true;
    }

    // 获取缓存
    public function get($key) {
        $filename = $this->getFilename($key);

        if (!file_exists($filename)) {
            return null;
        }

        $cacheData = unserialize(file_get_contents($filename));

        if (time() > $cacheData['expire']) {
            unlink($filename);
            return null;
        }

        return $cacheData['data'];
    }

    // 删除缓存
    public function delete($key) {
        $filename = $this->getFilename($key);
        if (file_exists($filename)) {
            return unlink($filename);
        }
        return true;
    }

    // 清空所有缓存
    public function clear() {
        $files = glob($this->cacheDir . '*');
        foreach ($files as $file) {
            if (is_file($file)) {
                unlink($file);
            }
        }
    }

    // 生成缓存文件名
    private function getFilename($key) {
        return $this->cacheDir . md5($key) . '.cache';
    }

    // 缓存数据库查询结果
    public function remember($key, $callback, $expire = null) {
        $data = $this->get($key);

        if ($data === null) {
            $data = call_user_func($callback);
            $this->set($key, $data, $expire);
        }

        return $data;
    }
}

2. 使用示例

// 在控制器中使用缓存
$cache = new Cache();

// 缓存热门文章
$popularArticles = $cache->remember('popular_articles', function() use ($blog) {
    return $blog->getPopularArticles(5);
}, 1800); // 30分钟缓存

// 缓存分类列表
$categories = $cache->remember('categories', function() use ($category) {
    return $category->getAll();
}, 3600); // 1小时缓存

部署和测试

1. 功能测试清单

## 博客系统测试清单

### 文章管理
- [ ] 创建文章
- [ ] 编辑文章
- [ ] 删除文章
- [ ] 文章发布/草稿状态切换
- [ ] 特色文章设置
- [ ] SEO字段设置
- [ ] 富文本编辑器
- [ ] 图片上传功能

### 分类和标签
- [ ] 创建分类
- [ ] 编辑分类
- [ ] 删除分类
- [ ] 分类层级关系
- [ ] 创建标签
- [ ] 标签与文章关联

### 评论系统
- [ ] 提交评论
- [ ] 评论审核
- [ ] 评论回复
- [ ] 垃圾评论过滤

### 搜索功能
- [ ] 全文搜索
- [ ] 分类搜索
- [ ] 标签搜索
- [ ] 搜索结果分页

### 性能优化
- [ ] 页面加载速度
- [ ] 数据库查询优化
- [ ] 缓存机制
- [ ] 图片优化

总结

通过开发博客系统,你已经:

  1. 掌握了复杂业务逻辑处理:学会了如何处理复杂的业务需求
  2. 实现了文件上传功能:了解了文件上传的安全性和处理方法
  3. 学会了分页显示:掌握了大量数据分页展示的技术
  4. 实现了搜索功能:学会了全文搜索的实现方法
  5. 集成了富文本编辑器:掌握了第三方库的集成方法
  6. 开发了后台管理系统:了解了完整的后台系统架构
  7. 实施了性能优化:学会了缓存等优化技术的使用
  8. 实践了SEO优化:了解了网站SEO的基本要求

这个博客系统是一个完整的PHP Web应用,它涵盖了现代Web开发的许多重要方面。你可以基于这个系统继续扩展,开发更多功能,如:

  • 用户关注系统
  • 文章收藏功能
  • 邮件订阅功能
  • 社交媒体集成
  • 多语言支持
  • 移动端适配