本文目录导读:

我来为你提供一个完整的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
实践是最好的学习方式!尝试自己扩展这个项目,添加更多功能,比如标签系统、好友关注、消息通知等,祝你学习顺利!
版权声明:NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播