Skip to content

15.1 实战 4:个人博客系统

本章节将实现一个完整的个人博客系统,包括文章列表、文章详情、分类管理、后台登录和文章增删改查功能。

项目结构

personal-blog/
├── db.php              # 数据库连接文件
├── index.php           # 首页(文章列表)
├── post.php            # 文章详情页
├── category.php        # 分类页面
├── admin/              # 后台目录
│   ├── index.php       # 后台登录页
│   ├── dashboard.php   # 后台仪表盘
│   ├── posts.php       # 文章管理
│   ├── categories.php  # 分类管理
│   ├── add-post.php    # 添加文章
│   ├── edit-post.php   # 编辑文章
│   └── logout.php      # 退出登录
└── css/
    └── style.css       # 样式文件

1. 数据库配置

创建 db.php 文件,用于数据库连接:

php
<?php
// db.php
function getDbConnection() {
    $servername = "localhost";
    $username = "root";
    $password = "";
    $dbname = "php_tutorial";
    
    // 创建连接
    $conn = new mysqli($servername, $username, $password, $dbname);
    
    // 检查连接
    if ($conn->connect_error) {
        die("连接失败: " . $conn->connect_error);
    }
    
    // 设置字符集
    $conn->set_charset("utf8mb4");
    
    return $conn;
}
?>

2. 数据库表结构

创建 categoriespostsusers 表:

sql
-- 分类表
CREATE TABLE categories (
    id INT AUTO_INCREMENT PRIMARY KEY,
    name VARCHAR(50) NOT NULL UNIQUE,
    slug VARCHAR(50) NOT NULL UNIQUE,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- 文章表
CREATE TABLE posts (
    id INT AUTO_INCREMENT PRIMARY KEY,
    title VARCHAR(100) NOT NULL,
    slug VARCHAR(100) NOT NULL UNIQUE,
    content TEXT NOT NULL,
    category_id INT,
    user_id INT,
    featured_image VARCHAR(255),
    status ENUM('published', 'draft') DEFAULT 'draft',
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    FOREIGN KEY (category_id) REFERENCES categories(id),
    FOREIGN KEY (user_id) REFERENCES users(id)
);

-- 用户表
CREATE TABLE users (
    id INT AUTO_INCREMENT PRIMARY KEY,
    username VARCHAR(50) NOT NULL UNIQUE,
    email VARCHAR(100) NOT NULL UNIQUE,
    password VARCHAR(255) NOT NULL,
    role ENUM('admin', 'author') DEFAULT 'author',
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- 插入测试数据
-- 插入分类
INSERT INTO categories (name, slug) VALUES
('技术', 'tech'),
('生活', 'life'),
('学习', 'study'),
('工作', 'work');

-- 插入用户
INSERT INTO users (username, email, password, role) VALUES
('admin', 'admin@example.com', 'password123', 'admin');

-- 插入文章
INSERT INTO posts (title, slug, content, category_id, user_id, status) VALUES
('PHP 入门指南', 'php-guide', 'PHP 是一种广泛使用的开源服务器端脚本语言...', 1, 1, 'published'),
('MySQL 数据库基础', 'mysql-basics', 'MySQL 是一种关系型数据库管理系统...', 1, 1, 'published'),
('生活随笔', 'life-essay', '今天天气很好,出去散步...', 2, 1, 'published'),
('学习心得', 'study-notes', '学习编程需要持之以恒...', 3, 1, 'published');

3. 样式文件

创建 css/style.css 文件:

css
/* style.css */
* {
    box-sizing: border-box;
    margin: 0;
    padding: 0;
}

body {
    font-family: Arial, sans-serif;
    line-height: 1.6;
    color: #333;
    background-color: #f4f4f4;
}

.container {
    max-width: 1200px;
    margin: 0 auto;
    padding: 20px;
}

header {
    background-color: #333;
    color: white;
    padding: 20px 0;
    margin-bottom: 30px;
}

.header-content {
    display: flex;
    justify-content: space-between;
    align-items: center;
}

.logo {
    font-size: 24px;
    font-weight: bold;
}

nav ul {
    list-style: none;
    display: flex;
    gap: 20px;
}

nav a {
    color: white;
    text-decoration: none;
    transition: color 0.3s ease;
}

nav a:hover {
    color: #4CAF50;
}

.main-content {
    display: flex;
    gap: 30px;
}

.content {
    flex: 3;
}

.sidebar {
    flex: 1;
}

.post {
    background-color: #fff;
    border-radius: 8px;
    box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
    padding: 20px;
    margin-bottom: 20px;
}

.post-title {
    font-size: 20px;
    font-weight: bold;
    margin-bottom: 10px;
}

.post-title a {
    color: #333;
    text-decoration: none;
    transition: color 0.3s ease;
}

.post-title a:hover {
    color: #4CAF50;
}

.post-meta {
    font-size: 14px;
    color: #999;
    margin-bottom: 15px;
}

.post-excerpt {
    margin-bottom: 15px;
    color: #666;
}

.post-category {
    display: inline-block;
    background-color: #4CAF50;
    color: white;
    padding: 3px 10px;
    border-radius: 12px;
    font-size: 12px;
    margin-bottom: 15px;
}

.read-more {
    display: inline-block;
    background-color: #333;
    color: white;
    padding: 8px 16px;
    border-radius: 4px;
    text-decoration: none;
    font-size: 14px;
    transition: background-color 0.3s ease;
}

.read-more:hover {
    background-color: #4CAF50;
}

.widget {
    background-color: #fff;
    border-radius: 8px;
    box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
    padding: 20px;
    margin-bottom: 20px;
}

.widget h3 {
    margin-bottom: 15px;
    font-size: 18px;
    border-bottom: 1px solid #eee;
    padding-bottom: 10px;
}

.category-list {
    list-style: none;
}

.category-list li {
    margin-bottom: 10px;
}

.category-list a {
    color: #333;
    text-decoration: none;
    transition: color 0.3s ease;
}

.category-list a:hover {
    color: #4CAF50;
}

.post-detail {
    background-color: #fff;
    border-radius: 8px;
    box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
    padding: 30px;
}

.post-detail h1 {
    font-size: 28px;
    margin-bottom: 20px;
}

.post-detail-content {
    margin-top: 30px;
    line-height: 1.8;
    color: #444;
}

.post-detail-content p {
    margin-bottom: 20px;
}

.pagination {
    margin-top: 30px;
    text-align: center;
}

.pagination a {
    display: inline-block;
    padding: 8px 16px;
    margin: 0 5px;
    background-color: #fff;
    border: 1px solid #ddd;
    border-radius: 4px;
    text-decoration: none;
    color: #333;
    transition: background-color 0.3s ease;
}

.pagination a:hover {
    background-color: #f0f0f0;
}

.pagination .active {
    background-color: #4CAF50;
    color: white;
    border: 1px solid #4CAF50;
}

footer {
    background-color: #333;
    color: white;
    padding: 20px 0;
    margin-top: 50px;
    text-align: center;
}

/* 后台样式 */
.admin-container {
    max-width: 1000px;
    margin: 0 auto;
    padding: 20px;
}

.admin-header {
    background-color: #333;
    color: white;
    padding: 10px 0;
    margin-bottom: 30px;
}

.admin-nav {
    background-color: #f8f9fa;
    padding: 10px;
    border-radius: 8px;
    margin-bottom: 30px;
}

.admin-nav ul {
    list-style: none;
    display: flex;
    gap: 20px;
}

.admin-nav a {
    color: #333;
    text-decoration: none;
    padding: 5px 10px;
    border-radius: 4px;
    transition: background-color 0.3s ease;
}

.admin-nav a:hover {
    background-color: #e9ecef;
}

.form-group {
    margin-bottom: 15px;
}

.form-group label {
    display: block;
    margin-bottom: 5px;
    font-weight: bold;
}

.form-group input[type="text"],
.form-group input[type="email"],
.form-group input[type="password"],
.form-group select,
.form-group textarea {
    width: 100%;
    padding: 10px;
    border: 1px solid #ddd;
    border-radius: 4px;
    font-size: 16px;
}

.form-group textarea {
    height: 200px;
    resize: vertical;
}

.btn {
    display: inline-block;
    padding: 10px 20px;
    border: none;
    border-radius: 4px;
    cursor: pointer;
    font-size: 16px;
    transition: background-color 0.3s ease;
}

.btn-primary {
    background-color: #4CAF50;
    color: white;
}

.btn-primary:hover {
    background-color: #45a049;
}

.btn-danger {
    background-color: #dc3545;
    color: white;
}

.btn-danger:hover {
    background-color: #c82333;
}

.table {
    width: 100%;
    border-collapse: collapse;
    margin-bottom: 30px;
}

.table th,
.table td {
    padding: 12px;
    text-align: left;
    border-bottom: 1px solid #ddd;
}

.table th {
    background-color: #f8f9fa;
    font-weight: bold;
}

.table tr:hover {
    background-color: #f8f9fa;
}

.alert {
    padding: 15px;
    margin-bottom: 20px;
    border-radius: 4px;
}

.alert-success {
    background-color: #d4edda;
    color: #155724;
    border: 1px solid #c3e6cb;
}

.alert-danger {
    background-color: #f8d7da;
    color: #721c24;
    border: 1px solid #f5c6cb;
}

.login-form {
    max-width: 400px;
    margin: 50px auto;
    background-color: #fff;
    padding: 30px;
    border-radius: 8px;
    box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}

.login-form h2 {
    text-align: center;
    margin-bottom: 30px;
}

/* 响应式设计 */
@media (max-width: 768px) {
    .main-content {
        flex-direction: column;
    }
    
    .header-content {
        flex-direction: column;
        gap: 10px;
    }
    
    nav ul {
        flex-wrap: wrap;
        justify-content: center;
    }
}

4. 首页(文章列表)

创建 index.php 文件:

php
<?php
// index.php
require_once 'db.php';
$conn = getDbConnection();

// 分页设置
$page = isset($_GET['page']) ? (int)$_GET['page'] : 1;
$perPage = 5;
$offset = ($page - 1) * $perPage;

// 获取文章总数
$countSql = "SELECT COUNT(*) as total FROM posts WHERE status = 'published'";
$countResult = $conn->query($countSql);
$countRow = $countResult->fetch_assoc();
$totalPosts = $countRow['total'];
$totalPages = ceil($totalPosts / $perPage);

// 获取文章列表
$sql = "SELECT posts.*, categories.name as category_name FROM posts JOIN categories ON posts.category_id = categories.id WHERE posts.status = 'published' ORDER BY posts.created_at DESC LIMIT ? OFFSET ?";
$stmt = $conn->prepare($sql);
$stmt->bind_param("ii", $perPage, $offset);
$stmt->execute();
$result = $stmt->get_result();

$posts = [];
if ($result->num_rows > 0) {
    while ($row = $result->fetch_assoc()) {
        $posts[] = $row;
    }
}

// 获取分类列表
$categorySql = "SELECT * FROM categories";
$categoryResult = $conn->query($categorySql);
$categories = [];
if ($categoryResult->num_rows > 0) {
    while ($row = $categoryResult->fetch_assoc()) {
        $categories[] = $row;
    }
}

$stmt->close();
$conn->close();
?>

<!DOCTYPE html>
<html>
<head>
    <title>个人博客</title>
    <link rel="stylesheet" href="css/style.css">
</head>
<body>
    <header>
        <div class="container header-content">
            <div class="logo">个人博客</div>
            <nav>
                <ul>
                    <li><a href="index.php">首页</a></li>
                    <?php foreach ($categories as $category): ?>
                        <li><a href="category.php?slug=<?php echo $category['slug']; ?>"><?php echo $category['name']; ?></a></li>
                    <?php endforeach; ?>
                </ul>
            </nav>
        </div>
    </header>
    
    <div class="container main-content">
        <div class="content">
            <?php if (empty($posts)): ?>
                <p>暂无文章</p>
            <?php else: ?>
                <?php foreach ($posts as $post): ?>
                    <div class="post">
                        <div class="post-category"><?php echo $post['category_name']; ?></div>
                        <h2 class="post-title"><a href="post.php?slug=<?php echo $post['slug']; ?>"><?php echo $post['title']; ?></a></h2>
                        <div class="post-meta">
                            发布于: <?php echo $post['created_at']; ?>
                        </div>
                        <div class="post-excerpt">
                            <?php echo substr(strip_tags($post['content']), 0, 200); ?>...
                        </div>
                        <a href="post.php?slug=<?php echo $post['slug']; ?>" class="read-more">阅读更多</a>
                    </div>
                <?php endforeach; ?>
            <?php endif; ?>
            
            <?php if ($totalPages > 1): ?>
                <div class="pagination">
                    <?php if ($page > 1): ?>
                        <a href="?page=<?php echo $page - 1; ?>">上一页</a>
                    <?php endif; ?>
                    
                    <?php for ($i = 1; $i <= $totalPages; $i++): ?>
                        <a href="?page=<?php echo $i; ?>" class="<?php echo $i == $page ? 'active' : ''; ?>">
                            <?php echo $i; ?>
                        </a>
                    <?php endfor; ?>
                    
                    <?php if ($page < $totalPages): ?>
                        <a href="?page=<?php echo $page + 1; ?>">下一页</a>
                    <?php endif; ?>
                </div>
            <?php endif; ?>
        </div>
        
        <div class="sidebar">
            <div class="widget">
                <h3>分类</h3>
                <ul class="category-list">
                    <?php foreach ($categories as $category): ?>
                        <li><a href="category.php?slug=<?php echo $category['slug']; ?>"><?php echo $category['name']; ?></a></li>
                    <?php endforeach; ?>
                </ul>
            </div>
            
            <div class="widget">
                <h3>关于我</h3>
                <p>这是一个个人博客,分享技术、生活和学习心得。</p>
            </div>
        </div>
    </div>
    
    <footer>
        <div class="container">
            <p>&copy; 2024 个人博客. 保留所有权利.</p>
        </div>
    </footer>
</body>
</html>

5. 文章详情页

创建 post.php 文件:

php
<?php
// post.php
require_once 'db.php';
$conn = getDbConnection();

$slug = isset($_GET['slug']) ? $_GET['slug'] : '';

// 获取文章详情
$sql = "SELECT posts.*, categories.name as category_name, categories.slug as category_slug FROM posts JOIN categories ON posts.category_id = categories.id WHERE posts.slug = ? AND posts.status = 'published'";
$stmt = $conn->prepare($sql);
$stmt->bind_param("s", $slug);
$stmt->execute();
$result = $stmt->get_result();

if ($result->num_rows === 0) {
    header('Location: index.php?error=文章不存在');
    exit;
}

$post = $result->fetch_assoc();

// 获取分类列表
$categorySql = "SELECT * FROM categories";
$categoryResult = $conn->query($categorySql);
$categories = [];
if ($categoryResult->num_rows > 0) {
    while ($row = $categoryResult->fetch_assoc()) {
        $categories[] = $row;
    }
}

$stmt->close();
$conn->close();
?>

<!DOCTYPE html>
<html>
<head>
    <title><?php echo $post['title']; ?> - 个人博客</title>
    <link rel="stylesheet" href="css/style.css">
</head>
<body>
    <header>
        <div class="container header-content">
            <div class="logo">个人博客</div>
            <nav>
                <ul>
                    <li><a href="index.php">首页</a></li>
                    <?php foreach ($categories as $category): ?>
                        <li><a href="category.php?slug=<?php echo $category['slug']; ?>"><?php echo $category['name']; ?></a></li>
                    <?php endforeach; ?>
                </ul>
            </nav>
        </div>
    </header>
    
    <div class="container main-content">
        <div class="content">
            <div class="post-detail">
                <div class="post-category"><a href="category.php?slug=<?php echo $post['category_slug']; ?>"><?php echo $post['category_name']; ?></a></div>
                <h1><?php echo $post['title']; ?></h1>
                <div class="post-meta">
                    发布于: <?php echo $post['created_at']; ?>
                    <?php if ($post['updated_at'] != $post['created_at']): ?>
                        | 更新于: <?php echo $post['updated_at']; ?>
                    <?php endif; ?>
                </div>
                <div class="post-detail-content">
                    <?php echo nl2br($post['content']); ?>
                </div>
            </div>
        </div>
        
        <div class="sidebar">
            <div class="widget">
                <h3>分类</h3>
                <ul class="category-list">
                    <?php foreach ($categories as $category): ?>
                        <li><a href="category.php?slug=<?php echo $category['slug']; ?>"><?php echo $category['name']; ?></a></li>
                    <?php endforeach; ?>
                </ul>
            </div>
            
            <div class="widget">
                <h3>关于我</h3>
                <p>这是一个个人博客,分享技术、生活和学习心得。</p>
            </div>
        </div>
    </div>
    
    <footer>
        <div class="container">
            <p>&copy; 2024 个人博客. 保留所有权利.</p>
        </div>
    </footer>
</body>
</html>

6. 分类页面

创建 category.php 文件:

php
<?php
// category.php
require_once 'db.php';
$conn = getDbConnection();

$slug = isset($_GET['slug']) ? $_GET['slug'] : '';

// 获取分类信息
$categorySql = "SELECT * FROM categories WHERE slug = ?";
$categoryStmt = $conn->prepare($categorySql);
$categoryStmt->bind_param("s", $slug);
$categoryStmt->execute();
$categoryResult = $categoryStmt->get_result();

if ($categoryResult->num_rows === 0) {
    header('Location: index.php?error=分类不存在');
    exit;
}

$category = $categoryResult->fetch_assoc();

// 分页设置
$page = isset($_GET['page']) ? (int)$_GET['page'] : 1;
$perPage = 5;
$offset = ($page - 1) * $perPage;

// 获取文章总数
$countSql = "SELECT COUNT(*) as total FROM posts WHERE category_id = ? AND status = 'published'";
$countStmt = $conn->prepare($countSql);
$countStmt->bind_param("i", $category['id']);
$countStmt->execute();
$countRow = $countStmt->get_result()->fetch_assoc();
$totalPosts = $countRow['total'];
$totalPages = ceil($totalPosts / $perPage);

// 获取文章列表
$sql = "SELECT * FROM posts WHERE category_id = ? AND status = 'published' ORDER BY created_at DESC LIMIT ? OFFSET ?";
$stmt = $conn->prepare($sql);
$stmt->bind_param("iii", $category['id'], $perPage, $offset);
$stmt->execute();
$result = $stmt->get_result();

$posts = [];
if ($result->num_rows > 0) {
    while ($row = $result->fetch_assoc()) {
        $posts[] = $row;
    }
}

// 获取所有分类
$allCategoriesSql = "SELECT * FROM categories";
$allCategoriesResult = $conn->query($allCategoriesSql);
$allCategories = [];
if ($allCategoriesResult->num_rows > 0) {
    while ($row = $allCategoriesResult->fetch_assoc()) {
        $allCategories[] = $row;
    }
}

$stmt->close();
$categoryStmt->close();
$countStmt->close();
$conn->close();
?>

<!DOCTYPE html>
<html>
<head>
    <title><?php echo $category['name']; ?> - 个人博客</title>
    <link rel="stylesheet" href="css/style.css">
</head>
<body>
    <header>
        <div class="container header-content">
            <div class="logo">个人博客</div>
            <nav>
                <ul>
                    <li><a href="index.php">首页</a></li>
                    <?php foreach ($allCategories as $cat): ?>
                        <li><a href="category.php?slug=<?php echo $cat['slug']; ?>"><?php echo $cat['name']; ?></a></li>
                    <?php endforeach; ?>
                </ul>
            </nav>
        </div>
    </header>
    
    <div class="container main-content">
        <div class="content">
            <h1>分类: <?php echo $category['name']; ?></h1>
            
            <?php if (empty($posts)): ?>
                <p>该分类下暂无文章</p>
            <?php else: ?>
                <?php foreach ($posts as $post): ?>
                    <div class="post">
                        <h2 class="post-title"><a href="post.php?slug=<?php echo $post['slug']; ?>"><?php echo $post['title']; ?></a></h2>
                        <div class="post-meta">
                            发布于: <?php echo $post['created_at']; ?>
                        </div>
                        <div class="post-excerpt">
                            <?php echo substr(strip_tags($post['content']), 0, 200); ?>...
                        </div>
                        <a href="post.php?slug=<?php echo $post['slug']; ?>" class="read-more">阅读更多</a>
                    </div>
                <?php endforeach; ?>
            <?php endif; ?>
            
            <?php if ($totalPages > 1): ?>
                <div class="pagination">
                    <?php if ($page > 1): ?>
                        <a href="?slug=<?php echo $slug; ?>&page=<?php echo $page - 1; ?>">上一页</a>
                    <?php endif; ?>
                    
                    <?php for ($i = 1; $i <= $totalPages; $i++): ?>
                        <a href="?slug=<?php echo $slug; ?>&page=<?php echo $i; ?>" class="<?php echo $i == $page ? 'active' : ''; ?>">
                            <?php echo $i; ?>
                        </a>
                    <?php endfor; ?>
                    
                    <?php if ($page < $totalPages): ?>
                        <a href="?slug=<?php echo $slug; ?>&page=<?php echo $page + 1; ?>">下一页</a>
                    <?php endif; ?>
                </div>
            <?php endif; ?>
        </div>
        
        <div class="sidebar">
            <div class="widget">
                <h3>分类</h3>
                <ul class="category-list">
                    <?php foreach ($allCategories as $cat): ?>
                        <li><a href="category.php?slug=<?php echo $cat['slug']; ?>"><?php echo $cat['name']; ?></a></li>
                    <?php endforeach; ?>
                </ul>
            </div>
            
            <div class="widget">
                <h3>关于我</h3>
                <p>这是一个个人博客,分享技术、生活和学习心得。</p>
            </div>
        </div>
    </div>
    
    <footer>
        <div class="container">
            <p>&copy; 2024 个人博客. 保留所有权利.</p>
        </div>
    </footer>
</body>
</html>

7. 后台登录页

创建 admin/index.php 文件:

php
<?php
// admin/index.php
session_start();

// 如果已登录,跳转到后台仪表盘
if (isset($_SESSION['user_id'])) {
    header('Location: dashboard.php');
    exit;
}

$error = '';

if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['login'])) {
    require_once '../db.php';
    $conn = getDbConnection();
    
    $username = $_POST['username'];
    $password = $_POST['password'];
    
    if (empty($username) || empty($password)) {
        $error = '用户名和密码不能为空';
    } else {
        $stmt = $conn->prepare("SELECT id, username, role FROM users WHERE username = ? AND password = ?");
        $stmt->bind_param("ss", $username, $password);
        $stmt->execute();
        $result = $stmt->get_result();
        
        if ($result->num_rows > 0) {
            $user = $result->fetch_assoc();
            $_SESSION['user_id'] = $user['id'];
            $_SESSION['username'] = $user['username'];
            $_SESSION['role'] = $user['role'];
            header('Location: dashboard.php');
            exit;
        } else {
            $error = '用户名或密码错误';
        }
        
        $stmt->close();
    }
    
    $conn->close();
}
?>

<!DOCTYPE html>
<html>
<head>
    <title>后台登录</title>
    <link rel="stylesheet" href="../css/style.css">
</head>
<body>
    <div class="container">
        <div class="login-form">
            <h2>后台登录</h2>
            
            <?php if (!empty($error)): ?>
                <div class="alert alert-danger"><?php echo $error; ?></div>
            <?php endif; ?>
            
            <form action="" method="post">
                <div class="form-group">
                    <label>用户名: <input type="text" name="username" required></label>
                </div>
                <div class="form-group">
                    <label>密码: <input type="password" name="password" required></label>
                </div>
                <button type="submit" name="login" class="btn btn-primary" style="width: 100%;">登录</button>
            </form>
        </div>
    </div>
</body>
</html>

8. 后台仪表盘

创建 admin/dashboard.php 文件:

php
<?php
// admin/dashboard.php
session_start();

// 检查是否登录
if (!isset($_SESSION['user_id'])) {
    header('Location: index.php');
    exit;
}

require_once '../db.php';
$conn = getDbConnection();

// 获取文章统计
$postCount = $conn->query("SELECT COUNT(*) as count FROM posts")->fetch_assoc()['count'];
$publishedCount = $conn->query("SELECT COUNT(*) as count FROM posts WHERE status = 'published'")->fetch_assoc()['count'];
$draftCount = $conn->query("SELECT COUNT(*) as count FROM posts WHERE status = 'draft'")->fetch_assoc()['count'];

// 获取分类统计
$categoryCount = $conn->query("SELECT COUNT(*) as count FROM categories")->fetch_assoc()['count'];

$conn->close();
?>

<!DOCTYPE html>
<html>
<head>
    <title>后台仪表盘</title>
    <link rel="stylesheet" href="../css/style.css">
</head>
<body>
    <div class="admin-header">
        <div class="container header-content">
            <div class="logo">博客后台</div>
            <nav>
                <ul>
                    <li><a href="dashboard.php">仪表盘</a></li>
                    <li><a href="posts.php">文章管理</a></li>
                    <li><a href="categories.php">分类管理</a></li>
                    <li><a href="logout.php">退出登录</a></li>
                </ul>
            </nav>
        </div>
    </div>
    
    <div class="admin-container">
        <h1>仪表盘</h1>
        
        <div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 20px; margin-top: 30px;">
            <div class="widget">
                <h3>总文章数</h3>
                <p style="font-size: 36px; font-weight: bold; color: #4CAF50;"><?php echo $postCount; ?></p>
            </div>
            <div class="widget">
                <h3>已发布</h3>
                <p style="font-size: 36px; font-weight: bold; color: #2196F3;"><?php echo $publishedCount; ?></p>
            </div>
            <div class="widget">
                <h3>草稿</h3>
                <p style="font-size: 36px; font-weight: bold; color: #FF9800;"><?php echo $draftCount; ?></p>
            </div>
            <div class="widget">
                <h3>分类数</h3>
                <p style="font-size: 36px; font-weight: bold; color: #9C27B0;"><?php echo $categoryCount; ?></p>
            </div>
        </div>
    </div>
</body>
</html>

9. 文章管理

创建 admin/posts.php 文件:

php
<?php
// admin/posts.php
session_start();

// 检查是否登录
if (!isset($_SESSION['user_id'])) {
    header('Location: index.php');
    exit;
}

require_once '../db.php';
$conn = getDbConnection();

// 删除文章
if (isset($_GET['delete'])) {
    $postId = (int)$_GET['delete'];
    $stmt = $conn->prepare("DELETE FROM posts WHERE id = ?");
    $stmt->bind_param("i", $postId);
    if ($stmt->execute()) {
        header('Location: posts.php?success=文章删除成功');
        exit;
    } else {
        header('Location: posts.php?error=文章删除失败');
        exit;
    }
}

// 获取文章列表
$sql = "SELECT posts.*, categories.name as category_name FROM posts JOIN categories ON posts.category_id = categories.id ORDER BY posts.created_at DESC";
$result = $conn->query($sql);

$posts = [];
if ($result->num_rows > 0) {
    while ($row = $result->fetch_assoc()) {
        $posts[] = $row;
    }
}

$conn->close();
?>

<!DOCTYPE html>
<html>
<head>
    <title>文章管理</title>
    <link rel="stylesheet" href="../css/style.css">
</head>
<body>
    <div class="admin-header">
        <div class="container header-content">
            <div class="logo">博客后台</div>
            <nav>
                <ul>
                    <li><a href="dashboard.php">仪表盘</a></li>
                    <li><a href="posts.php">文章管理</a></li>
                    <li><a href="categories.php">分类管理</a></li>
                    <li><a href="logout.php">退出登录</a></li>
                </ul>
            </nav>
        </div>
    </div>
    
    <div class="admin-container">
        <h1>文章管理</h1>
        
        <?php if (isset($_GET['success'])): ?>
            <div class="alert alert-success"><?php echo $_GET['success']; ?></div>
        <?php endif; ?>
        
        <?php if (isset($_GET['error'])): ?>
            <div class="alert alert-danger"><?php echo $_GET['error']; ?></div>
        <?php endif; ?>
        
        <a href="add-post.php" class="btn btn-primary" style="margin-bottom: 20px;">添加文章</a>
        
        <table class="table">
            <thead>
                <tr>
                    <th>ID</th>
                    <th>标题</th>
                    <th>分类</th>
                    <th>状态</th>
                    <th>发布时间</th>
                    <th>操作</th>
                </tr>
            </thead>
            <tbody>
                <?php if (empty($posts)): ?>
                    <tr>
                        <td colspan="6" style="text-align: center;">暂无文章</td>
                    </tr>
                <?php else: ?>
                    <?php foreach ($posts as $post): ?>
                        <tr>
                            <td><?php echo $post['id']; ?></td>
                            <td><?php echo $post['title']; ?></td>
                            <td><?php echo $post['category_name']; ?></td>
                            <td><?php echo $post['status'] === 'published' ? '已发布' : '草稿'; ?></td>
                            <td><?php echo $post['created_at']; ?></td>
                            <td>
                                <a href="edit-post.php?id=<?php echo $post['id']; ?>" class="btn btn-primary" style="padding: 5px 10px; font-size: 14px;">编辑</a>
                                <a href="?delete=<?php echo $post['id']; ?>" class="btn btn-danger" style="padding: 5px 10px; font-size: 14px; margin-left: 10px;" onclick="return confirm('确定要删除这篇文章吗?');">删除</a>
                            </td>
                        </tr>
                    <?php endforeach; ?>
                <?php endif; ?>
            </tbody>
        </table>
    </div>
</body>
</html>

10. 添加文章

创建 admin/add-post.php 文件:

php
<?php
// admin/add-post.php
session_start();

// 检查是否登录
if (!isset($_SESSION['user_id'])) {
    header('Location: index.php');
    exit;
}

require_once '../db.php';
$conn = getDbConnection();

$error = '';
$success = '';

// 获取分类列表
$categorySql = "SELECT * FROM categories";
$categoryResult = $conn->query($categorySql);
$categories = [];
if ($categoryResult->num_rows > 0) {
    while ($row = $categoryResult->fetch_assoc()) {
        $categories[] = $row;
    }
}

if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['submit'])) {
    $title = $_POST['title'];
    $slug = $_POST['slug'];
    $content = $_POST['content'];
    $categoryId = $_POST['category_id'];
    $status = $_POST['status'];
    $userId = $_SESSION['user_id'];
    
    if (empty($title) || empty($slug) || empty($content) || empty($categoryId)) {
        $error = '标题、别名、内容和分类不能为空';
    } else {
        // 检查 slug 是否已存在
        $stmt = $conn->prepare("SELECT id FROM posts WHERE slug = ?");
        $stmt->bind_param("s", $slug);
        $stmt->execute();
        $result = $stmt->get_result();
        
        if ($result->num_rows > 0) {
            $error = '别名已存在,请使用其他别名';
        } else {
            // 插入文章
            $stmt = $conn->prepare("INSERT INTO posts (title, slug, content, category_id, user_id, status) VALUES (?, ?, ?, ?, ?, ?)");
            $stmt->bind_param("sssiss", $title, $slug, $content, $categoryId, $userId, $status);
            
            if ($stmt->execute()) {
                $success = '文章添加成功';
                // 清空表单
                $title = $slug = $content = '';
                $categoryId = 1;
                $status = 'draft';
            } else {
                $error = '文章添加失败,请重试';
            }
        }
        
        $stmt->close();
    }
}

$conn->close();
?>

<!DOCTYPE html>
<html>
<head>
    <title>添加文章</title>
    <link rel="stylesheet" href="../css/style.css">
</head>
<body>
    <div class="admin-header">
        <div class="container header-content">
            <div class="logo">博客后台</div>
            <nav>
                <ul>
                    <li><a href="dashboard.php">仪表盘</a></li>
                    <li><a href="posts.php">文章管理</a></li>
                    <li><a href="categories.php">分类管理</a></li>
                    <li><a href="logout.php">退出登录</a></li>
                </ul>
            </nav>
        </div>
    </div>
    
    <div class="admin-container">
        <h1>添加文章</h1>
        
        <?php if (!empty($error)): ?>
            <div class="alert alert-danger"><?php echo $error; ?></div>
        <?php endif; ?>
        
        <?php if (!empty($success)): ?>
            <div class="alert alert-success"><?php echo $success; ?></div>
        <?php endif; ?>
        
        <form action="" method="post">
            <div class="form-group">
                <label>标题: <input type="text" name="title" value="<?php echo isset($title) ? htmlspecialchars($title) : ''; ?>" required></label>
            </div>
            <div class="form-group">
                <label>别名: <input type="text" name="slug" value="<?php echo isset($slug) ? htmlspecialchars($slug) : ''; ?>" required></label>
                <small>用于URL,建议使用小写字母、数字和连字符</small>
            </div>
            <div class="form-group">
                <label>分类: 
                    <select name="category_id" required>
                        <?php foreach ($categories as $category): ?>
                            <option value="<?php echo $category['id']; ?>" <?php echo isset($categoryId) && $categoryId == $category['id'] ? 'selected' : ''; ?>>
                                <?php echo $category['name']; ?>
                            </option>
                        <?php endforeach; ?>
                    </select>
                </label>
            </div>
            <div class="form-group">
                <label>内容: <textarea name="content" required><?php echo isset($content) ? htmlspecialchars($content) : ''; ?></textarea></label>
            </div>
            <div class="form-group">
                <label>状态: 
                    <select name="status">
                        <option value="draft" <?php echo isset($status) && $status == 'draft' ? 'selected' : ''; ?>>草稿</option>
                        <option value="published" <?php echo isset($status) && $status == 'published' ? 'selected' : ''; ?>>已发布</option>
                    </select>
                </label>
            </div>
            <button type="submit" name="submit" class="btn btn-primary">保存</button>
            <a href="posts.php" class="btn" style="background-color: #6c757d; color: white; margin-left: 10px;">取消</a>
        </form>
    </div>
</body>
</html>

11. 编辑文章

创建 admin/edit-post.php 文件:

php
<?php
// admin/edit-post.php
session_start();

// 检查是否登录
if (!isset($_SESSION['user_id'])) {
    header('Location: index.php');
    exit;
}

require_once '../db.php';
$conn = getDbConnection();

$postId = isset($_GET['id']) ? (int)$_GET['id'] : 0;

// 获取文章详情
$stmt = $conn->prepare("SELECT * FROM posts WHERE id = ?");
$stmt->bind_param("i", $postId);
$stmt->execute();
$result = $stmt->get_result();

if ($result->num_rows === 0) {
    header('Location: posts.php?error=文章不存在');
    exit;
}

$post = $result->fetch_assoc();

// 获取分类列表
$categorySql = "SELECT * FROM categories";
$categoryResult = $conn->query($categorySql);
$categories = [];
if ($categoryResult->num_rows > 0) {
    while ($row = $categoryResult->fetch_assoc()) {
        $categories[] = $row;
    }
}

$error = '';
$success = '';

if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['submit'])) {
    $title = $_POST['title'];
    $slug = $_POST['slug'];
    $content = $_POST['content'];
    $categoryId = $_POST['category_id'];
    $status = $_POST['status'];
    
    if (empty($title) || empty($slug) || empty($content) || empty($categoryId)) {
        $error = '标题、别名、内容和分类不能为空';
    } else {
        // 检查 slug 是否已存在(排除当前文章)
        $stmt = $conn->prepare("SELECT id FROM posts WHERE slug = ? AND id != ?");
        $stmt->bind_param("si", $slug, $postId);
        $stmt->execute();
        $result = $stmt->get_result();
        
        if ($result->num_rows > 0) {
            $error = '别名已存在,请使用其他别名';
        } else {
            // 更新文章
            $stmt = $conn->prepare("UPDATE posts SET title = ?, slug = ?, content = ?, category_id = ?, status = ? WHERE id = ?");
            $stmt->bind_param("sssisi", $title, $slug, $content, $categoryId, $status, $postId);
            
            if ($stmt->execute()) {
                $success = '文章更新成功';
                // 更新变量
                $post['title'] = $title;
                $post['slug'] = $slug;
                $post['content'] = $content;
                $post['category_id'] = $categoryId;
                $post['status'] = $status;
            } else {
                $error = '文章更新失败,请重试';
            }
        }
        
        $stmt->close();
    }
}

$conn->close();
?>

<!DOCTYPE html>
<html>
<head>
    <title>编辑文章</title>
    <link rel="stylesheet" href="../css/style.css">
</head>
<body>
    <div class="admin-header">
        <div class="container header-content">
            <div class="logo">博客后台</div>
            <nav>
                <ul>
                    <li><a href="dashboard.php">仪表盘</a></li>
                    <li><a href="posts.php">文章管理</a></li>
                    <li><a href="categories.php">分类管理</a></li>
                    <li><a href="logout.php">退出登录</a></li>
                </ul>
            </nav>
        </div>
    </div>
    
    <div class="admin-container">
        <h1>编辑文章</h1>
        
        <?php if (!empty($error)): ?>
            <div class="alert alert-danger"><?php echo $error; ?></div>
        <?php endif; ?>
        
        <?php if (!empty($success)): ?>
            <div class="alert alert-success"><?php echo $success; ?></div>
        <?php endif; ?>
        
        <form action="" method="post">
            <div class="form-group">
                <label>标题: <input type="text" name="title" value="<?php echo htmlspecialchars($post['title']); ?>" required></label>
            </div>
            <div class="form-group">
                <label>别名: <input type="text" name="slug" value="<?php echo htmlspecialchars($post['slug']); ?>" required></label>
                <small>用于URL,建议使用小写字母、数字和连字符</small>
            </div>
            <div class="form-group">
                <label>分类: 
                    <select name="category_id" required>
                        <?php foreach ($categories as $category): ?>
                            <option value="<?php echo $category['id']; ?>" <?php echo $post['category_id'] == $category['id'] ? 'selected' : ''; ?>>
                                <?php echo $category['name']; ?>
                            </option>
                        <?php endforeach; ?>
                    </select>
                </label>
            </div>
            <div class="form-group">
                <label>内容: <textarea name="content" required><?php echo htmlspecialchars($post['content']); ?></textarea></label>
            </div>
            <div class="form-group">
                <label>状态: 
                    <select name="status">
                        <option value="draft" <?php echo $post['status'] == 'draft' ? 'selected' : ''; ?>>草稿</option>
                        <option value="published" <?php echo $post['status'] == 'published' ? 'selected' : ''; ?>>已发布</option>
                    </select>
                </label>
            </div>
            <button type="submit" name="submit" class="btn btn-primary">保存</button>
            <a href="posts.php" class="btn" style="background-color: #6c757d; color: white; margin-left: 10px;">取消</a>
        </form>
    </div>
</body>
</html>

12. 分类管理

创建 admin/categories.php 文件:

php
<?php
// admin/categories.php
session_start();

// 检查是否登录
if (!isset($_SESSION['user_id'])) {
    header('Location: index.php');
    exit;
}

require_once '../db.php';
$conn = getDbConnection();

// 添加分类
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['add'])) {
    $name = $_POST['name'];
    $slug = $_POST['slug'];
    
    if (empty($name) || empty($slug)) {
        $error = '分类名称和别名不能为空';
    } else {
        // 检查名称是否已存在
        $stmt = $conn->prepare("SELECT id FROM categories WHERE name = ?");
        $stmt->bind_param("s", $name);
        $stmt->execute();
        $result = $stmt->get_result();
        
        if ($result->num_rows > 0) {
            $error = '分类名称已存在';
        } else {
            // 检查别名是否已存在
            $stmt = $conn->prepare("SELECT id FROM categories WHERE slug = ?");
            $stmt->bind_param("s", $slug);
            $stmt->execute();
            $result = $stmt->get_result();
            
            if ($result->num_rows > 0) {
                $error = '分类别名已存在';
            } else {
                // 插入分类
                $stmt = $conn->prepare("INSERT INTO categories (name, slug) VALUES (?, ?)");
                $stmt->bind_param("ss", $name, $slug);
                
                if ($stmt->execute()) {
                    $success = '分类添加成功';
                } else {
                    $error = '分类添加失败,请重试';
                }
            }
        }
        
        $stmt->close();
    }
}

// 删除分类
if (isset($_GET['delete'])) {
    $categoryId = (int)$_GET['delete'];
    
    // 检查分类是否被文章使用
    $stmt = $conn->prepare("SELECT id FROM posts WHERE category_id = ?");
    $stmt->bind_param("i", $categoryId);
    $stmt->execute();
    $result = $stmt->get_result();
    
    if ($result->num_rows > 0) {
        header('Location: categories.php?error=该分类下有文章,无法删除');
        exit;
    } else {
        $stmt = $conn->prepare("DELETE FROM categories WHERE id = ?");
        $stmt->bind_param("i", $categoryId);
        if ($stmt->execute()) {
            header('Location: categories.php?success=分类删除成功');
            exit;
        } else {
            header('Location: categories.php?error=分类删除失败');
            exit;
        }
    }
}

// 获取分类列表
$sql = "SELECT * FROM categories ORDER BY name";
$result = $conn->query($sql);

$categories = [];
if ($result->num_rows > 0) {
    while ($row = $result->fetch_assoc()) {
        $categories[] = $row;
    }
}

$conn->close();
?>

<!DOCTYPE html>
<html>
<head>
    <title>分类管理</title>
    <link rel="stylesheet" href="../css/style.css">
</head>
<body>
    <div class="admin-header">
        <div class="container header-content">
            <div class="logo">博客后台</div>
            <nav>
                <ul>
                    <li><a href="dashboard.php">仪表盘</a></li>
                    <li><a href="posts.php">文章管理</a></li>
                    <li><a href="categories.php">分类管理</a></li>
                    <li><a href="logout.php">退出登录</a></li>
                </ul>
            </nav>
        </div>
    </div>
    
    <div class="admin-container">
        <h1>分类管理</h1>
        
        <?php if (isset($_GET['success']) || isset($success)): ?>
            <div class="alert alert-success"><?php echo isset($success) ? $success : $_GET['success']; ?></div>
        <?php endif; ?>
        
        <?php if (isset($_GET['error']) || isset($error)): ?>
            <div class="alert alert-danger"><?php echo isset($error) ? $error : $_GET['error']; ?></div>
        <?php endif; ?>
        
        <h2>添加分类</h2>
        <form action="" method="post" style="margin-bottom: 30px;">
            <div style="display: flex; gap: 10px; align-items: end;">
                <div class="form-group" style="flex: 1;">
                    <label>分类名称: <input type="text" name="name" required></label>
                </div>
                <div class="form-group" style="flex: 1;">
                    <label>别名: <input type="text" name="slug" required></label>
                    <small>用于URL,建议使用小写字母、数字和连字符</small>
                </div>
                <div style="margin-bottom: 15px;">
                    <button type="submit" name="add" class="btn btn-primary">添加</button>
                </div>
            </div>
        </form>
        
        <h2>分类列表</h2>
        <table class="table">
            <thead>
                <tr>
                    <th>ID</th>
                    <th>名称</th>
                    <th>别名</th>
                    <th>创建时间</th>
                    <th>操作</th>
                </tr>
            </thead>
            <tbody>
                <?php if (empty($categories)): ?>
                    <tr>
                        <td colspan="5" style="text-align: center;">暂无分类</td>
                    </tr>
                <?php else: ?>
                    <?php foreach ($categories as $category): ?>
                        <tr>
                            <td><?php echo $category['id']; ?></td>
                            <td><?php echo $category['name']; ?></td>
                            <td><?php echo $category['slug']; ?></td>
                            <td><?php echo $category['created_at']; ?></td>
                            <td>
                                <a href="?delete=<?php echo $category['id']; ?>" class="btn btn-danger" style="padding: 5px 10px; font-size: 14px;" onclick="return confirm('确定要删除这个分类吗?');">删除</a>
                            </td>
                        </tr>
                    <?php endforeach; ?>
                <?php endif; ?>
            </tbody>
        </table>
    </div>
</body>
</html>

13. 退出登录

创建 admin/logout.php 文件:

php
<?php
// admin/logout.php
session_start();

// 清除会话变量
session_unset();

// 销毁会话
session_destroy();

// 跳转到登录页面
header('Location: index.php');
exit;
?>

14. 运行项目

  1. 确保数据库已创建并创建了所需的表,插入了测试数据
  2. 启动本地服务器
  3. 访问 http://localhost/personal-blog/index.php 查看前台
  4. 访问 http://localhost/personal-blog/admin/index.php 登录后台(用户名: admin, 密码: password123)
  5. 在后台管理文章和分类

15. 功能扩展

  1. 添加富文本编辑器:使用 CKEditor 或 TinyMCE 等富文本编辑器编辑文章内容
  2. 添加图片上传功能:支持文章特色图片上传
  3. 添加评论功能:允许用户在文章下方评论
  4. 添加标签功能:为文章添加标签
  5. 添加搜索功能:支持文章搜索
  6. 添加分页功能:后台文章列表添加分页
  7. 添加用户管理:支持多用户管理
  8. 添加主题切换:支持前台主题切换

16. 注意事项

  1. 安全性

    • 使用 htmlspecialchars 防止 XSS 攻击
    • 使用预处理语句防止 SQL 注入
    • 验证用户输入
    • 使用会话管理用户状态
  2. 性能

    • 使用分页减少数据传输
    • 优化数据库查询
    • 合理使用会话
  3. 用户体验

    • 提供清晰的错误提示
    • 显示操作成功的反馈
    • 友好的界面设计
    • 响应式设计,适配不同屏幕尺寸
  4. 功能完整性

    • 实现文章列表展示
    • 实现文章详情页
    • 实现分类管理
    • 实现后台登录
    • 实现文章增删改查

练习

  1. 实现完整的个人博客系统
  2. 添加富文本编辑器
  3. 添加图片上传功能
  4. 添加评论功能
  5. 优化用户界面和体验

© 2026 编程马·菜鸟教程 版权所有