本文目录导读:

我来为你提供一个完整的PHP项目实战教程,从基础到进阶,包含实际项目案例。
📚 PHP项目实战教程
项目准备阶段
环境搭建
# 推荐使用集成环境 - XAMPP (Windows) - MAMP (Mac) - LAMP (Linux) # 或者使用Docker docker run -p 80:80 -p 3306:3306 --name php-env -d \ -v $(pwd)/www:/var/www/html \ php:8.2-apache
项目结构设计
project/
├── public/ # 公共目录
│ ├── index.php # 入口文件
│ ├── css/
│ ├── js/
│ └── images/
├── app/ # 应用目录
│ ├── controllers/
│ ├── models/
│ ├── views/
│ └── helpers/
├── config/ # 配置文件
├── database/ # 数据库文件
└── vendor/ # 第三方库
实战项目:博客管理系统
数据库设计
-- users 用户表
CREATE TABLE users (
id INT PRIMARY KEY AUTO_INCREMENT,
username VARCHAR(50) UNIQUE NOT NULL,
email VARCHAR(100) UNIQUE NOT NULL,
password VARCHAR(255) NOT NULL,
avatar VARCHAR(255),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- posts 文章表
CREATE TABLE posts (
id INT PRIMARY KEY AUTO_INCREMENT,
user_id INT,VARCHAR(200) NOT NULL,
content TEXT,
status ENUM('draft', 'published') DEFAULT 'draft',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id)
);
-- comments 评论表
CREATE TABLE comments (
id INT PRIMARY KEY AUTO_INCREMENT,
post_id INT,
user_id INT,
content TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (post_id) REFERENCES posts(id),
FOREIGN KEY (user_id) REFERENCES users(id)
);
配置文件 (config/database.php)
<?php
// 数据库配置
define('DB_HOST', 'localhost');
define('DB_NAME', 'blog_db');
define('DB_USER', 'root');
define('DB_PASS', '');
define('DB_CHARSET', 'utf8mb4');
// 网站配置
define('SITE_NAME', '我的博客');
define('BASE_URL', 'http://localhost/blog/');
// 会话配置
define('SESSION_LIFETIME', 3600);
// 数据库连接类
class Database {
private static $instance = null;
private $connection;
private function __construct() {
try {
$dsn = "mysql:host=" . DB_HOST . ";dbname=" . DB_NAME . ";charset=" . DB_CHARSET;
$this->connection = new PDO($dsn, DB_USER, DB_PASS);
$this->connection->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
} catch (PDOException $e) {
die("数据库连接失败: " . $e->getMessage());
}
}
public static function getInstance() {
if (self::$instance === null) {
self::$instance = new Database();
}
return self::$instance;
}
public function getConnection() {
return $this->connection;
}
}
用户认证类 (app/models/User.php)
<?php
class User {
private $db;
public function __construct() {
$this->db = Database::getInstance()->getConnection();
}
// 用户注册
public function register($username, $email, $password) {
// 验证输入
if (empty($username) || empty($email) || empty($password)) {
return ['success' => false, 'message' => '所有字段都是必填的'];
}
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
return ['success' => false, 'message' => '邮箱格式不正确'];
}
// 检查用户名/邮箱是否已存在
$stmt = $this->db->prepare("SELECT id FROM users WHERE username = ? OR email = ?");
$stmt->execute([$username, $email]);
if ($stmt->rowCount() > 0) {
return ['success' => false, 'message' => '用户名或邮箱已被使用'];
}
// 密码加密
$hashedPassword = password_hash($password, PASSWORD_DEFAULT);
// 插入用户
$stmt = $this->db->prepare(
"INSERT INTO users (username, email, password) VALUES (?, ?, ?)"
);
try {
$stmt->execute([$username, $email, $hashedPassword]);
return ['success' => true, 'user_id' => $this->db->lastInsertId()];
} catch (PDOException $e) {
return ['success' => false, 'message' => '注册失败: ' . $e->getMessage()];
}
}
// 用户登录
public function login($username, $password) {
$stmt = $this->db->prepare("SELECT * FROM users WHERE username = ?");
$stmt->execute([$username]);
$user = $stmt->fetch(PDO::FETCH_ASSOC);
if ($user && password_verify($password, $user['password'])) {
// 登录成功,创建会话
$_SESSION['user_id'] = $user['id'];
$_SESSION['username'] = $user['username'];
$_SESSION['logged_in'] = true;
return ['success' => true, 'user' => $user];
}
return ['success' => false, 'message' => '用户名或密码错误'];
}
// 退出登录
public function logout() {
session_unset();
session_destroy();
return true;
}
// 获取用户信息
public function getUserById($userId) {
$stmt = $this->db->prepare("SELECT id, username, email, avatar, created_at FROM users WHERE id = ?");
$stmt->execute([$userId]);
return $stmt->fetch(PDO::FETCH_ASSOC);
}
}
文章管理类 (app/models/Post.php)
<?php
class Post {
private $db;
public function __construct() {
$this->db = Database::getInstance()->getConnection();
}
// 创建文章
public function create($userId, $title, $content, $status = 'draft') {
$stmt = $this->db->prepare(
"INSERT INTO posts (user_id, title, content, status) VALUES (?, ?, ?, ?)"
);
try {
$stmt->execute([$userId, $title, $content, $status]);
return ['success' => true, 'post_id' => $this->db->lastInsertId()];
} catch (PDOException $e) {
return ['success' => false, 'message' => $e->getMessage()];
}
}
// 获取所有文章(分页)
public function getAllPosts($page = 1, $limit = 10) {
$offset = ($page - 1) * $limit;
$stmt = $this->db->prepare(
"SELECT p.*, u.username
FROM posts p
JOIN users u ON p.user_id = u.id
WHERE p.status = 'published'
ORDER BY p.created_at DESC
LIMIT :limit OFFSET :offset"
);
$stmt->bindValue(':limit', $limit, PDO::PARAM_INT);
$stmt->bindValue(':offset', $offset, PDO::PARAM_INT);
$stmt->execute();
return $stmt->fetchAll(PDO::FETCH_ASSOC);
}
// 获取单篇文章
public function getPostById($postId) {
$stmt = $this->db->prepare(
"SELECT p.*, u.username
FROM posts p
JOIN users u ON p.user_id = u.id
WHERE p.id = ?"
);
$stmt->execute([$postId]);
return $stmt->fetch(PDO::FETCH_ASSOC);
}
// 更新文章
public function update($postId, $userId, $title, $content, $status) {
$stmt = $this->db->prepare(
"UPDATE posts
SET title = ?, content = ?, status = ?
WHERE id = ? AND user_id = ?"
);
try {
$stmt->execute([$title, $content, $status, $postId, $userId]);
return ['success' => true];
} catch (PDOException $e) {
return ['success' => false, 'message' => $e->getMessage()];
}
}
// 删除文章
public function delete($postId, $userId) {
// 先删除关联的评论
$stmt = $this->db->prepare("DELETE FROM comments WHERE post_id = ?");
$stmt->execute([$postId]);
// 再删除文章
$stmt = $this->db->prepare("DELETE FROM posts WHERE id = ? AND user_id = ?");
try {
$stmt->execute([$postId, $userId]);
return ['success' => true];
} catch (PDOException $e) {
return ['success' => false, 'message' => $e->getMessage()];
}
}
// 搜索文章
public function search($keyword, $page = 1, $limit = 10) {
$offset = ($page - 1) * $limit;
$searchTerm = "%{$keyword}%";
$stmt = $this->db->prepare(
"SELECT p.*, u.username
FROM posts p
JOIN users u ON p.user_id = u.id
WHERE p.status = 'published'
AND (p.title LIKE ? OR p.content LIKE ?)
ORDER BY p.created_at DESC
LIMIT :limit OFFSET :offset"
);
$stmt->bindValue(':limit', $limit, PDO::PARAM_INT);
$stmt->bindValue(':offset', $offset, PDO::PARAM_INT);
$stmt->execute([$searchTerm, $searchTerm]);
return $stmt->fetchAll(PDO::FETCH_ASSOC);
}
}
控制器示例 (app/controllers/PostController.php)
<?php
require_once '../models/Post.php';
require_once '../models/Comment.php';
class PostController {
private $postModel;
private $commentModel;
public function __construct() {
$this->postModel = new Post();
$this->commentModel = new Comment();
}
// 显示文章列表
public function index() {
$page = isset($_GET['page']) ? (int)$_GET['page'] : 1;
$posts = $this->postModel->getAllPosts($page);
// 加载视图
require_once '../views/posts/index.php';
}
// 显示单篇文章
public function show($id) {
$post = $this->postModel->getPostById($id);
$comments = $this->commentModel->getCommentsByPostId($id);
if (!$post) {
header('HTTP/1.0 404 Not Found');
die('文章不存在');
}
require_once '../views/posts/show.php';
}
// 创建文章表单
public function create() {
// 检查登录状态
if (!isset($_SESSION['logged_in'])) {
header('Location: /blog/login');
exit;
}
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
// 处理表单提交
$title = $_POST['title'];
$content = $_POST['content'];
$status = $_POST['status'] ?? 'draft';
$result = $this->postModel->create(
$_SESSION['user_id'],
$title,
$content,
$status
);
if ($result['success']) {
header('Location: /blog/posts/' . $result['post_id']);
exit;
} else {
$error = $result['message'];
}
}
require_once '../views/posts/create.php';
}
// 更新文章
public function update($id) {
// 获取原始文章数据
$post = $this->postModel->getPostById($id);
// 权限检查
if (!$post || $post['user_id'] != $_SESSION['user_id']) {
header('HTTP/1.0 403 Forbidden');
die('没有权限执行此操作');
}
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$title = $_POST['title'];
$content = $_POST['content'];
$status = $_POST['status'] ?? 'draft';
$result = $this->postModel->update(
$id,
$_SESSION['user_id'],
$title,
$content,
$status
);
if ($result['success']) {
header('Location: /blog/posts/' . $id);
exit;
}
}
require_once '../views/posts/edit.php';
}
// 删除文章
public function delete($id) {
$post = $this->postModel->getPostById($id);
// 权限检查
if (!$post || $post['user_id'] != $_SESSION['user_id']) {
header('HTTP/1.0 403 Forbidden');
die('没有权限执行此操作');
}
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$result = $this->postModel->delete($id, $_SESSION['user_id']);
header('Location: /blog/posts');
exit;
}
require_once '../views/posts/delete.php';
}
// 搜索文章
public function search() {
$keyword = $_GET['q'] ?? '';
$page = isset($_GET['page']) ? (int)$_GET['page'] : 1;
$posts = $this->postModel->search($keyword, $page);
require_once '../views/posts/search.php';
}
}
路由配置 (public/index.php)
<?php
// 开启会话
session_start();
// 加载配置
require_once '../config/database.php';
require_once '../helpers/functions.php';
// 简单路由处理
$request = $_SERVER['REQUEST_URI'];
$path = parse_url($request, PHP_URL_PATH);
$method = $_SERVER['REQUEST_METHOD'];
// 移除BASE_URL前缀
$path = str_replace('/blog', '', $path);
$segments = explode('/', trim($path, '/'));
// 默认路由
$controller = 'Home';
$action = 'index';
$params = [];
// 解析路由
if (!empty($segments[0])) {
$controller = ucfirst($segments[0]);
if (!empty($segments[1])) {
$action = $segments[1];
}
if (count($segments) > 2) {
$params = array_slice($segments, 2);
}
}
// 控制器文件路径
$controllerFile = "../app/controllers/{$controller}Controller.php";
if (file_exists($controllerFile)) {
require_once $controllerFile;
$controllerClass = "{$controller}Controller";
if (class_exists($controllerClass)) {
$controllerInstance = new $controllerClass();
if (method_exists($controllerInstance, $action)) {
call_user_func_array([$controllerInstance, $action], $params);
} else {
http_response_code(404);
echo "页面不存在";
}
} else {
http_response_code(404);
echo "控制器类不存在";
}
} else {
http_response_code(404);
echo "控制器文件不存在";
}
视图模板 (app/views/posts/index.php)
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">文章列表 - <?php echo SITE_NAME; ?></title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: "Microsoft YaHei", sans-serif; background: #f5f5f5; }
.container { max-width: 1200px; margin: 0 auto; padding: 20px; }
.header { background: #333; color: white; padding: 20px; }
.nav { background: #444; padding: 10px; }
.nav a { color: white; text-decoration: none; margin-right: 15px; }
.post { background: white; padding: 20px; margin-bottom: 20px; border-radius: 5px; }
.post-title { font-size: 24px; margin-bottom: 10px; color: #333; }
.post-meta { color: #777; font-size: 14px; margin-bottom: 10px; }
.post-content { color: #555; line-height: 1.6; }
.footer { text-align: center; padding: 20px; color: #777; background: #333; }
</style>
</head>
<body>
<div class="header">
<h1><?php echo SITE_NAME; ?></h1>
</div>
<div class="nav">
<a href="/blog/">首页</a>
<a href="/blog/posts">文章列表</a>
<a href="/blog/search">搜索</a>
<?php if (isset($_SESSION['logged_in'])): ?>
<a href="/blog/posts/create">写文章</a>
<a href="/blog/user/dashboard"><?php echo $_SESSION['username']; ?></a>
<a href="/blog/user/logout">退出</a>
<?php else: ?>
<a href="/blog/user/login">登录</a>
<a href="/blog/user/register">注册</a>
<?php endif; ?>
</div>
<div class="container">
<h2>最新文章</h2>
<?php if (empty($posts)): ?>
<p>暂无文章</p>
<?php else: ?>
<?php foreach ($posts as $post): ?>
<div class="post">
<h3 class="post-title">
<a href="/blog/posts/show/<?php echo $post['id']; ?>">
<?php echo htmlspecialchars($post['title']); ?>
</a>
</h3>
<div class="post-meta">
作者:<?php echo htmlspecialchars($post['username']); ?> |
发布时间:<?php echo date('Y-m-d H:i', strtotime($post['created_at'])); ?>
</div>
<div class="post-content">
<?php echo mb_substr(strip_tags($post['content']), 0, 200); ?>...
</div>
</div>
<?php endforeach; ?>
<!-- 分页 -->
<div class="pagination">
<?php if ($page > 1): ?>
<a href="/blog/posts?page=<?php echo $page - 1; ?>" class="btn">上一页</a>
<?php endif; ?>
<?php if (count($posts) == 10): ?>
<a href="/blog/posts?page=<?php echo $page + 1; ?>" class="btn">下一页</a>
<?php endif; ?>
</div>
<?php endif; ?>
</div>
<div class="footer">
<p>© <?php echo date('Y'); ?> <?php echo SITE_NAME; ?></p>
</div>
</body>
</html>
辅助函数 (app/helpers/functions.php)
<?php
// 安全输出函数
function e($string) {
return htmlspecialchars($string, ENT_QUOTES, 'UTF-8');
}
// 重定向函数
function redirect($url) {
header("Location: " . BASE_URL . $url);
exit;
}
// 生成CSRF令牌
function generateCSRFToken() {
if (empty($_SESSION['csrf_token'])) {
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
}
return $_SESSION['csrf_token'];
}
// 验证CSRF令牌
function validateCSRFToken($token) {
return isset($_SESSION['csrf_token']) && $token === $_SESSION['csrf_token'];
}
// 检查用户是否登录
function isLoggedIn() {
return isset($_SESSION['logged_in']) && $_SESSION['logged_in'] === true;
}
// 获取当前用户ID
function getCurrentUserId() {
return $_SESSION['user_id'] ?? null;
}
// 上传文件处理
function handleFileUpload($file, $targetDir, $allowedTypes = ['jpg', 'jpeg', 'png', 'gif']) {
$targetDir = rtrim($targetDir, '/') . '/';
// 创建目录
if (!file_exists($targetDir)) {
mkdir($targetDir, 0777, true);
}
// 检查文件类型
$fileExt = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));
if (!in_array($fileExt, $allowedTypes)) {
return ['success' => false, 'message' => '不支持的图片类型'];
}
// 生成新文件名
$newFileName = uniqid() . '.' . $fileExt;
$targetPath = $targetDir . $newFileName;
if (move_uploaded_file($file['tmp_name'], $targetPath)) {
return ['success' => true, 'path' => $targetPath];
}
return ['success' => false, 'message' => '文件上传失败'];
}
// 分页函数
function paginate($totalItems, $perPage = 10, $currentPage = 1) {
$totalPages = ceil($totalItems / $perPage);
$currentPage = max(1, min($currentPage, $totalPages));
return [
'totalItems' => $totalItems,
'totalPages' => $totalPages,
'currentPage' => $currentPage,
'perPage' => $perPage,
'offset' => ($currentPage - 1) * $perPage
];
}
// 发送JSON响应
function jsonResponse($data, $statusCode = 200) {
http_response_code($statusCode);
header('Content-Type: application/json');
echo json_encode($data);
exit;
}
// 获取客户端IP
function getClientIP() {
$ip = '';
if (!empty($_SERVER['HTTP_CLIENT_IP'])) {
$ip = $_SERVER['HTTP_CLIENT_IP'];
} elseif (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
$ip = $_SERVER['HTTP_X_FORWARDED_FOR'];
} else {
$ip = $_SERVER['REMOTE_ADDR'] ?? '';
}
return $ip;
}
安全最佳实践
<?php
// 1. SQL注入防护 - 使用PDO预处理
class SecureDatabase {
private $connection;
public function __construct() {
$dsn = "mysql:host=" . DB_HOST . ";dbname=" . DB_NAME . ";charset=utf8mb4";
$options = [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_EMULATE_PREPARES => false,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC
];
$this->connection = new PDO($dsn, DB_USER, DB_PASS, $options);
}
}
// 2. XSS防护 - 输出过滤
function cleanOutput($data) {
return htmlspecialchars($data, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
}
// 3. CSRF防护 - 表单令牌
class CSRFProtection {
public static function generateToken() {
if (empty($_SESSION['csrf_token'])) {
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
}
return $_SESSION['csrf_token'];
}
public static function validateToken($token) {
return hash_equals($_SESSION['csrf_token'] ?? '', $token);
}
}
// 4. 文件上传安全
function secureFileUpload($file) {
// 检查文件大小
if ($file['size'] > 2 * 1024 * 1024) { // 2MB
throw new Exception('文件太大');
}
// 检查MIME类型
$finfo = new finfo(FILEINFO_MIME_TYPE);
$mimeType = $finfo->file($file['tmp_name']);
$allowedMimes = [
'image/jpeg',
'image/png',
'image/gif',
'application/pdf'
];
if (!in_array($mimeType, $allowedMimes)) {
throw new Exception('文件类型不允许');
}
return true;
}
性能优化技巧
<?php
// 1. 缓存配置
class Cache {
private $cacheDir;
public function __construct($cacheDir = 'cache/') {
$this->cacheDir = $cacheDir;
if (!file_exists($cacheDir)) {
mkdir($cacheDir, 0777, true);
}
}
public function set($key, $data, $ttl = 3600) {
$filename = $this->cacheDir . md5($key) . '.cache';
$content = json_encode([
'expires' => time() + $ttl,
'data' => $data
]);
file_put_contents($filename, $content, LOCK_EX);
}
public function get($key) {
$filename = $this->cacheDir . md5($key) . '.cache';
if (file_exists($filename)) {
$content = json_decode(file_get_contents($filename), true);
if ($content['expires'] > time()) {
return $content['data'];
}
// 缓存过期,删除文件
unlink($filename);
}
return null;
}
}
// 2. 数据库查询优化
class QueryOptimizer {
public function getPostsWithComments($limit = 10) {
// 使用JOIN代替多次查询
$sql = "
SELECT
p.*,
u.username,
COUNT(c.id) as comment_count
FROM posts p
LEFT JOIN users u ON p.user_id = u.id
LEFT JOIN comments c ON c.post_id = p.id
GROUP BY p.id
ORDER BY p.created_at DESC
LIMIT ?
";
$stmt = $this->db->prepare($sql);
$stmt->execute([$limit]);
return $stmt->fetchAll();
}
}
// 3. Gzip压缩
if (ob_get_level() == 0) {
ob_start('ob_gzhandler');
}
// 4. 静态文件版本控制
function asset($file) {
$path = BASE_PATH . 'public/' . $file;
$version = filemtime($path);
return BASE_URL . $file . '?v=' . $version;
}
部署与运维
# 1. 环境检查脚本 #!/bin/bash echo "检查PHP版本..." php -v echo "检查PHP扩展..." php -m | grep -i "pdo\|mysqli\|openssl" echo "检查MySQL..." mysql --version echo "设置权限..." chmod -R 755 public/ chmod -R 777 uploads/ cache/ chmod 644 config/*.php # 2. 部署脚本 #!/bin/bash # 备份 backup_dir="backups/$(date +%Y%m%d)" mkdir -p $backup_dir mysqldump -u root -p blog_db > $backup_dir/database.sql tar -czf $backup_dir/files.tar.gz public/ app/ config/ # 更新代码 git pull origin main # 更新数据库 php migrations/run.php # 清理缓存 rm -rf cache/* echo "部署完成!" # 3. 定时任务示例 (crontab) # 每天凌晨2点备份 0 2 * * * /backup/backup.sh # 清理过期会话 */30 * * * * php /var/www/html/maintenance/clean_sessions.php
调试与测试
<?php
// 1. 错误日志
function logError($message, $type = 'ERROR') {
$logPath = __DIR__ . '/../logs/';
if (!file_exists($logPath)) {
mkdir($logPath, 0777, true);
}
$filename = $logPath . date('Y-m-d') . '.log';
$timestamp = date('Y-m-d H:i:s');
$entry = "[$timestamp] [$type] $message" . PHP_EOL;
file_put_contents($filename, $entry, FILE_APPEND | LOCK_EX);
}
// 2. 单元测试示例
class PostTest extends PHPUnit\Framework\TestCase {
private $post;
protected function setUp(): void {
$this->post = new Post();
}
public function testCreatePost() {
$result = $this->post->create(1, '测试文章', '这是测试内容');
$this->assertTrue($result['success']);
}
public function testGetAllPosts() {
$posts = $this->post->getAllPosts();
$this->assertIsArray($posts);
$this->assertCount(10, $posts);
}
}
// 3. Xdebug配置 (php.ini)
/*
[xdebug]
xdebug.mode=debug
xdebug.start_with_request=yes
xdebug.client_host=127.0.0.1
xdebug.client_port=9000
xdebug.log=/var/log/xdebug.log
*/
通过这个实战教程,你学到了:
- MVC架构模式:如何组织代码结构
- 数据库操作:PDO安全连接和查询
- 用户认证系统:注册、登录、权限控制
- CRUD操作:文章的创建、读取、更新、删除
- 安全防护:SQL注入、XSS、CSRF防护
- 性能优化:缓存、查询优化、Gzip压缩
- 部署运维:备份、更新、定时任务
📌 推荐学习路径
- 基础阶段:HTML/CSS → PHP基础 → MySQL
- 进阶阶段:OOP → 模式设计 → 安全编程
- 实战阶段:独立完成小型项目 → 参与开源
- 框架学习:Laravel / ThinkPHP / Symfony
📚 资源推荐
- 官方文档: php.net
- 在线教程: W3Schools, PHP The Right Way
- 框架指南: Laravel Official Docs
- 代码规范: PSR-1, PSR-4, PSR-12
实践是最好的学习方式!尝试自己扩展这个项目,添加更多功能,比如标签系统、好友关注、消息通知等,祝你学习顺利!